Compare commits
No commits in common. "main" and "compose-tor-ollama" have entirely different histories.
main
...
compose-to
264
.acf/tasks.json
264
.acf/tasks.json
|
|
@ -1,264 +0,0 @@
|
|||
{
|
||||
"projectName": "metal-kompanion-mcp",
|
||||
"projectDescription": "MCP backend for Kompanion: memory/context/embedding provider over MCP, built from scratch (qtmcp-based) to persist conversation state and serve embeddings + retrieval to avoid forgetting across threads.",
|
||||
"lastTaskId": 24,
|
||||
"tasks": [
|
||||
{
|
||||
"id": 22,
|
||||
"title": "Handlers → DAL integration",
|
||||
"description": "Wire kom.memory.v1.upsert_memory/search_memory to IDatabase. Parse JSON with a real parser, validate against schemas, enforce scope and sensitivity rules.",
|
||||
"status": "todo",
|
||||
"priority": 490,
|
||||
"priorityDisplay": "P0",
|
||||
"dependsOn": [],
|
||||
"createdAt": "2025-10-14T00:30:26.285Z",
|
||||
"updatedAt": "2025-10-14T00:30:26.285Z",
|
||||
"subtasks": [
|
||||
{
|
||||
"id": "22.1",
|
||||
"title": "Replace ad-hoc JSON with parser (nlohmann/json or simdjson)",
|
||||
"status": "todo",
|
||||
"createdAt": "2025-10-14T00:30:33.761Z",
|
||||
"updatedAt": "2025-10-14T00:30:33.761Z",
|
||||
"activityLog": [
|
||||
{
|
||||
"timestamp": "2025-10-14T00:30:33.761Z",
|
||||
"type": "log",
|
||||
"message": "Subtask created with title: \"Replace ad-hoc JSON with parser (nlohmann/json or simdjson)\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "22.2",
|
||||
"title": "Validate request bodies against schemas before DAL calls",
|
||||
"status": "todo",
|
||||
"createdAt": "2025-10-14T00:30:39.868Z",
|
||||
"updatedAt": "2025-10-14T00:30:39.868Z",
|
||||
"activityLog": [
|
||||
{
|
||||
"timestamp": "2025-10-14T00:30:39.868Z",
|
||||
"type": "log",
|
||||
"message": "Subtask created with title: \"Validate request bodies against schemas before DAL calls\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "22.3",
|
||||
"title": "Scope & sensitivity enforcement (namespace/user + skip secret embeddings)",
|
||||
"status": "todo",
|
||||
"createdAt": "2025-10-14T00:30:45.261Z",
|
||||
"updatedAt": "2025-10-14T00:30:45.261Z",
|
||||
"activityLog": [
|
||||
{
|
||||
"timestamp": "2025-10-14T00:30:45.261Z",
|
||||
"type": "log",
|
||||
"message": "Subtask created with title: \"Scope & sensitivity enforcement (namespace/user + skip secret embeddings)\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"lastSubtaskIndex": 3,
|
||||
"relatedFiles": [],
|
||||
"activityLog": [
|
||||
{
|
||||
"timestamp": "2025-10-14T00:30:26.285Z",
|
||||
"type": "log",
|
||||
"message": "Task created with title: \"Handlers → DAL integration\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"title": "Contract tests: DAL-backed tools",
|
||||
"description": "Expand CTest to cover DAL-backed upsert/search and backup export/import; include error cases and schema violations; run against build-komhands.",
|
||||
"status": "todo",
|
||||
"priority": 511,
|
||||
"priorityDisplay": "P1",
|
||||
"dependsOn": [],
|
||||
"createdAt": "2025-10-14T00:30:51.716Z",
|
||||
"updatedAt": "2025-10-14T00:30:51.716Z",
|
||||
"subtasks": [],
|
||||
"lastSubtaskIndex": 0,
|
||||
"relatedFiles": [],
|
||||
"activityLog": [
|
||||
{
|
||||
"timestamp": "2025-10-14T00:30:51.716Z",
|
||||
"type": "log",
|
||||
"message": "Task created with title: \"Contract tests: DAL-backed tools\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"title": "Implement KompanionAI SDK",
|
||||
"description": "",
|
||||
"status": "todo",
|
||||
"priority": 489,
|
||||
"priorityDisplay": "medium",
|
||||
"dependsOn": [],
|
||||
"createdAt": "2025-10-16T09:24:13.006Z",
|
||||
"updatedAt": "2025-10-16T09:30:49.564Z",
|
||||
"subtasks": [
|
||||
{
|
||||
"id": "24.1",
|
||||
"title": "Define Message & Thread Model",
|
||||
"status": "done",
|
||||
"createdAt": "2025-10-16T09:25:41.659Z",
|
||||
"updatedAt": "2025-10-16T09:30:49.396Z",
|
||||
"activityLog": [
|
||||
{
|
||||
"timestamp": "2025-10-16T09:25:41.660Z",
|
||||
"type": "log",
|
||||
"message": "Subtask created with title: \"Define Message & Thread Model\""
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-10-16T09:30:49.396Z",
|
||||
"type": "log",
|
||||
"message": "Status changed from \"todo\" to \"done\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "24.2",
|
||||
"title": "Implement Tool / Function Calling",
|
||||
"status": "done",
|
||||
"createdAt": "2025-10-16T09:25:41.835Z",
|
||||
"updatedAt": "2025-10-16T09:30:49.564Z",
|
||||
"activityLog": [
|
||||
{
|
||||
"timestamp": "2025-10-16T09:25:41.835Z",
|
||||
"type": "log",
|
||||
"message": "Subtask created with title: \"Implement Tool / Function Calling\""
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-10-16T09:30:49.564Z",
|
||||
"type": "log",
|
||||
"message": "Status changed from \"todo\" to \"done\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "24.3",
|
||||
"title": "Implement Provider abstraction (multi-backend)",
|
||||
"status": "todo",
|
||||
"createdAt": "2025-10-16T09:25:42.021Z",
|
||||
"updatedAt": "2025-10-16T09:25:42.021Z",
|
||||
"activityLog": [
|
||||
{
|
||||
"timestamp": "2025-10-16T09:25:42.021Z",
|
||||
"type": "log",
|
||||
"message": "Subtask created with title: \"Implement Provider abstraction (multi-backend)\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "24.4",
|
||||
"title": "Implement Completion / Reply / Streaming Events",
|
||||
"status": "todo",
|
||||
"createdAt": "2025-10-16T09:25:42.197Z",
|
||||
"updatedAt": "2025-10-16T09:25:42.197Z",
|
||||
"activityLog": [
|
||||
{
|
||||
"timestamp": "2025-10-16T09:25:42.197Z",
|
||||
"type": "log",
|
||||
"message": "Subtask created with title: \"Implement Completion / Reply / Streaming Events\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "24.5",
|
||||
"title": "Implement Options / Policies / Privacy",
|
||||
"status": "todo",
|
||||
"createdAt": "2025-10-16T09:25:42.371Z",
|
||||
"updatedAt": "2025-10-16T09:25:42.371Z",
|
||||
"activityLog": [
|
||||
{
|
||||
"timestamp": "2025-10-16T09:25:42.371Z",
|
||||
"type": "log",
|
||||
"message": "Subtask created with title: \"Implement Options / Policies / Privacy\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "24.6",
|
||||
"title": "Implement Embeddings (for RAG / memory)",
|
||||
"status": "todo",
|
||||
"createdAt": "2025-10-16T09:25:42.547Z",
|
||||
"updatedAt": "2025-10-16T09:25:42.547Z",
|
||||
"activityLog": [
|
||||
{
|
||||
"timestamp": "2025-10-16T09:25:42.547Z",
|
||||
"type": "log",
|
||||
"message": "Subtask created with title: \"Implement Embeddings (for RAG / memory)\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "24.7",
|
||||
"title": "Implement Agent Loop Conveniences",
|
||||
"status": "todo",
|
||||
"createdAt": "2025-10-16T09:25:42.723Z",
|
||||
"updatedAt": "2025-10-16T09:25:42.724Z",
|
||||
"activityLog": [
|
||||
{
|
||||
"timestamp": "2025-10-16T09:25:42.724Z",
|
||||
"type": "log",
|
||||
"message": "Subtask created with title: \"Implement Agent Loop Conveniences\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "24.8",
|
||||
"title": "Implement Error Model & Cancellation",
|
||||
"status": "todo",
|
||||
"createdAt": "2025-10-16T09:25:42.898Z",
|
||||
"updatedAt": "2025-10-16T09:25:42.898Z",
|
||||
"activityLog": [
|
||||
{
|
||||
"timestamp": "2025-10-16T09:25:42.898Z",
|
||||
"type": "log",
|
||||
"message": "Subtask created with title: \"Implement Error Model & Cancellation\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "24.9",
|
||||
"title": "Expose to QML",
|
||||
"status": "todo",
|
||||
"createdAt": "2025-10-16T09:25:43.075Z",
|
||||
"updatedAt": "2025-10-16T09:25:43.075Z",
|
||||
"activityLog": [
|
||||
{
|
||||
"timestamp": "2025-10-16T09:25:43.075Z",
|
||||
"type": "log",
|
||||
"message": "Subtask created with title: \"Expose to QML\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "24.10",
|
||||
"title": "Migrate KLLM to KompanionAI",
|
||||
"status": "todo",
|
||||
"createdAt": "2025-10-16T09:25:43.252Z",
|
||||
"updatedAt": "2025-10-16T09:25:43.252Z",
|
||||
"activityLog": [
|
||||
{
|
||||
"timestamp": "2025-10-16T09:25:43.252Z",
|
||||
"type": "log",
|
||||
"message": "Subtask created with title: \"Migrate KLLM to KompanionAI\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"lastSubtaskIndex": 10,
|
||||
"relatedFiles": [],
|
||||
"activityLog": [
|
||||
{
|
||||
"timestamp": "2025-10-16T09:24:13.006Z",
|
||||
"type": "log",
|
||||
"message": "Task created with title: \"Implement KompanionAI SDK\""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
---
|
||||
# SPDX-FileCopyrightText: 2019 Christoph Cullmann <cullmann@kde.org>
|
||||
# SPDX-FileCopyrightText: 2019 Gernot Gebhard <gebhard@absint.com>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
# This file got automatically created by ECM, do not edit
|
||||
# See https://clang.llvm.org/docs/ClangFormatStyleOptions.html for the config options
|
||||
# and https://community.kde.org/Policies/Frameworks_Coding_Style#Clang-format_automatic_code_formatting
|
||||
# for clang-format tips & tricks
|
||||
---
|
||||
Language: JavaScript
|
||||
DisableFormat: true
|
||||
---
|
||||
Language: Json
|
||||
DisableFormat: false
|
||||
IndentWidth: 4
|
||||
---
|
||||
|
||||
# Style for C++
|
||||
Language: Cpp
|
||||
|
||||
# base is WebKit coding style: https://webkit.org/code-style-guidelines/
|
||||
# below are only things set that diverge from this style!
|
||||
BasedOnStyle: WebKit
|
||||
|
||||
# enforce C++11 (e.g. for std::vector<std::vector<lala>>
|
||||
Standard: Cpp11
|
||||
|
||||
# 4 spaces indent
|
||||
TabWidth: 4
|
||||
|
||||
# 2 * 80 wide lines
|
||||
ColumnLimit: 160
|
||||
|
||||
# sort includes inside line separated groups
|
||||
SortIncludes: true
|
||||
|
||||
# break before braces on function, namespace and class definitions.
|
||||
BreakBeforeBraces: Linux
|
||||
|
||||
# CrlInstruction *a;
|
||||
PointerAlignment: Right
|
||||
|
||||
# horizontally aligns arguments after an open bracket.
|
||||
AlignAfterOpenBracket: Align
|
||||
|
||||
# don't move all parameters to new line
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
|
||||
# no single line functions
|
||||
AllowShortFunctionsOnASingleLine: None
|
||||
|
||||
# no single line enums
|
||||
AllowShortEnumsOnASingleLine: false
|
||||
|
||||
# always break before you encounter multi line strings
|
||||
AlwaysBreakBeforeMultilineStrings: true
|
||||
|
||||
# don't move arguments to own lines if they are not all on the same
|
||||
BinPackArguments: false
|
||||
|
||||
# don't move parameters to own lines if they are not all on the same
|
||||
BinPackParameters: false
|
||||
|
||||
# In case we have an if statement with multiple lines the operator should be at the beginning of the line
|
||||
# but we do not want to break assignments
|
||||
BreakBeforeBinaryOperators: NonAssignment
|
||||
|
||||
# format C++11 braced lists like function calls
|
||||
Cpp11BracedListStyle: true
|
||||
|
||||
# do not put a space before C++11 braced lists
|
||||
SpaceBeforeCpp11BracedList: false
|
||||
|
||||
# remove empty lines
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
|
||||
# no namespace indentation to keep indent level low
|
||||
NamespaceIndentation: None
|
||||
|
||||
# we use template< without space.
|
||||
SpaceAfterTemplateKeyword: false
|
||||
|
||||
# Always break after template declaration
|
||||
AlwaysBreakTemplateDeclarations: true
|
||||
|
||||
# macros for which the opening brace stays attached.
|
||||
ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH, forever, Q_FOREVER, QBENCHMARK, QBENCHMARK_ONCE , wl_resource_for_each, wl_resource_for_each_safe ]
|
||||
|
||||
# keep lambda formatting multi-line if not empty
|
||||
AllowShortLambdasOnASingleLine: Empty
|
||||
|
||||
# We do not want clang-format to put all arguments on a new line
|
||||
AllowAllArgumentsOnNextLine: false
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# Cursor AI Workflow Rules for Agentic Control Framework
|
||||
|
||||
# (Define rules here to tell Cursor how to use acf commands)
|
||||
|
||||
# Example:
|
||||
# To list tasks, use the command: acf list
|
||||
# To get the next task: acf next
|
||||
45
AGENTS.md
45
AGENTS.md
|
|
@ -1,45 +0,0 @@
|
|||
# Repository Guidelines
|
||||
This guide supports new agents contributing to `metal-kompanion`, the MCP backend for Kompanion. Follow these practices to keep the service buildable, testable, and easy to review.
|
||||
|
||||
|
||||
source dev.env for envrionment variables.
|
||||
|
||||
## MCP Usage
|
||||
- This project uses agentic-control-framework Use this for task planning
|
||||
|
||||
## Project Structure & Module Organization
|
||||
- `src/` holds C++ code: `mcp/` for server facade and tool routing, `memory/` for embeddings contracts, `dal/` for persistence, and `policy/` for capability rules.
|
||||
- `docs/` hosts design notes; `runtime/kom_runner.py` is the Python orchestrator for agent execution against Ollama; `db/` and `sql/` capture Postgres schemas.
|
||||
- `docs/third_party` is a symlink to reference code and apidocs.
|
||||
- Place sample payloads or fixtures under `tests/` (currently `tests/schemas/` for JSON samples). Generated artifacts live in `build/`.
|
||||
- `src/cli` is a command line prompt interface that tests the memory integration and pattern
|
||||
substiton.
|
||||
- `tools`
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
```bash
|
||||
cmake -S . -B build # configure with CMake 3.22+, targets C++20
|
||||
cmake --build build -j # compile the kom_mcp executable
|
||||
ctest --test-dir build # run CTest suites once they are defined
|
||||
python3 runtime/kom_runner.py # exercise the runtime loop (requires OLLAMA_BASE)
|
||||
```
|
||||
Run the Python runtime from a virtualenv seeded with `pip install -r runtime/requirements.txt`.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- C++20, clang/gcc friendly, 4-space indentation, braces on the same line (`class KomMcpServer {`).
|
||||
- Classes use PascalCase; methods camelCase; private members keep the trailing underscore (`tools_`). Prefer `std::` types and RAII helpers over raw pointers.
|
||||
- Keep headers lean: forward declare where possible and document non-obvious behavior with concise comments.
|
||||
|
||||
## Testing Guidelines
|
||||
- Add unit or schema validation tests under `tests/`, mirroring the source tree (e.g., `tests/mcp/` for dispatcher tests).
|
||||
- Register new tests with CTest via `add_test` in `CMakeLists.txt`; verify they pass with `ctest --output-on-failure`.
|
||||
- Provide realistic JSON samples for new tools alongside schema updates to guard against regressions.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
- Follow the existing `scope: message` pattern from git history (`docker: fix host ollama port`); keep messages imperative and present tense.
|
||||
- Each PR should state intent, link relevant issues, and include before/after notes or screenshots when UI-adjacent runtime behavior changes.
|
||||
- Mention how to reproduce test runs (`cmake --build`, `ctest`) and flag configuration or migration steps (e.g., `sql/` changes).
|
||||
|
||||
## Security & Configuration Tips
|
||||
- Do not commit secrets; runtime state lives beneath XDG paths (`~/.local/state/kompanion`). Document any new env vars in `docs/`.
|
||||
- When integrating new tools, ensure access control aligns with `policy/` capabilities and record expected journal entries in `runtime` docs.
|
||||
|
|
@ -1,68 +1,10 @@
|
|||
cmake_minimum_required(VERSION 3.22)
|
||||
project(Kompanion LANGUAGES CXX)
|
||||
set(PROJECT_VERSION "0.0.1")
|
||||
|
||||
set(QT_MIN_VERSION "6.0.0")
|
||||
set(KF6_MIN_VERSION "6.0.0")
|
||||
set(KDE_COMPILERSETTINGS_LEVEL "5.82")
|
||||
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(MetalKompanion LANGUAGES CXX)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
find_package(ECM ${KF6_MIN_VERSION} REQUIRED NO_MODULE)
|
||||
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
||||
|
||||
include(KDEInstallDirs)
|
||||
include(KDECMakeSettings)
|
||||
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||
include(ECMMarkAsTest)
|
||||
include(ECMMarkNonGuiExecutable)
|
||||
include(FeatureSummary)
|
||||
include(CheckIncludeFile)
|
||||
include(CheckIncludeFiles)
|
||||
include(CheckSymbolExists)
|
||||
include(ECMOptionalAddSubdirectory)
|
||||
include(KDEClangFormat)
|
||||
include(ECMDeprecationSettings)
|
||||
|
||||
include(KDEGitCommitHooks)
|
||||
find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS
|
||||
Core
|
||||
Network
|
||||
Sql
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Network DBus)
|
||||
add_executable(kompanion_server
|
||||
src/main.cpp
|
||||
)
|
||||
|
||||
find_package(Qt6McpServer CONFIG REQUIRED)
|
||||
find_package(Qt6McpCommon CONFIG REQUIRED)
|
||||
|
||||
find_package(Qt6 ${KF6_MIN_VERSON} CONFIG REQUIRED COMPONENTS Gui)
|
||||
find_package(KF6Config ${KF6_MIN_VERSION} CONFIG REQUIRED)
|
||||
find_package(KF6Parts ${KF6_MIN_VERSION} CONFIG REQUIRED)
|
||||
find_package(KF6TextEditor ${KF6_MIN_VERSION} CONFIG REQUIRED)
|
||||
|
||||
find_package(Qt6Test ${QT_MIN_VERSION} CONFIG QUIET)
|
||||
set_package_properties(Qt6Test PROPERTIES
|
||||
PURPOSE "Required for tests"
|
||||
TYPE OPTIONAL
|
||||
)
|
||||
add_feature_info("Qt6Test" Qt6Test_FOUND "Required for building tests")
|
||||
|
||||
|
||||
set(KOMPANION_DB_INIT_INSTALL_DIR "${KDE_INSTALL_FULL_DATADIR}/kompanion/db/init")
|
||||
install(DIRECTORY db/init/ DESTINATION ${KDE_INSTALL_DATADIR}/kompanion/db/init FILES_MATCHING PATTERN "*.sql")
|
||||
|
||||
add_subdirectory(src)
|
||||
|
||||
option(BUILD_TESTS "Build tests" ON)
|
||||
|
||||
if (BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
find_program(MCP_PROXY_EXECUTABLE mcp-proxy)
|
||||
if (MCP_PROXY_EXECUTABLE)
|
||||
message(STATUS "Found mcp-proxy: ${MCP_PROXY_EXECUTABLE}")
|
||||
endif()
|
||||
|
||||
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
||||
target_link_libraries(kompanion_server Qt6::Core Qt6::Network Qt6::DBus)
|
||||
install(TARGETS kompanion_server RUNTIME DESTINATION bin)
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
docker compose -f docker/compose.host.yml up -d
|
||||
|
|
@ -1 +0,0 @@
|
|||
CREATE DATABASE kompanion OWNER kompanion;
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS namespaces (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT UNIQUE NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS memory_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
namespace_id UUID NOT NULL REFERENCES namespaces(id) ON DELETE CASCADE,
|
||||
key TEXT,
|
||||
content TEXT NOT NULL,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
tags TEXT[] NOT NULL DEFAULT '{}',
|
||||
revision INT NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
expires_at TIMESTAMPTZ,
|
||||
deleted_at TIMESTAMPTZ,
|
||||
last_accessed_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS memory_chunks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
item_id UUID NOT NULL REFERENCES memory_items(id) ON DELETE CASCADE,
|
||||
seq INT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
expires_at TIMESTAMPTZ,
|
||||
deleted_at TIMESTAMPTZ,
|
||||
content_tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED
|
||||
);
|
||||
|
||||
-- Ensure single row per (item,seq)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_chunks_item_seq ON memory_chunks(item_id, seq);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS embeddings (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
chunk_id UUID NOT NULL REFERENCES memory_chunks(id) ON DELETE CASCADE,
|
||||
model TEXT NOT NULL,
|
||||
dim INT NOT NULL,
|
||||
vector VECTOR(1024),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE(chunk_id, model)
|
||||
);
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
-- Auth secrets for bearer token authentication
|
||||
CREATE TABLE IF NOT EXISTS auth_secrets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
namespace_id UUID NOT NULL REFERENCES namespaces(id) ON DELETE CASCADE,
|
||||
secret_hash TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE(namespace_id)
|
||||
);
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
-- Create the dev_knowledge namespace if it doesn't exist
|
||||
INSERT INTO namespaces (name) VALUES ('dev_knowledge') ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- Create a secret for the dev_knowledge namespace for testing
|
||||
DO $$
|
||||
DECLARE
|
||||
ns_id UUID;
|
||||
BEGIN
|
||||
SELECT id INTO ns_id FROM namespaces WHERE name = 'dev_knowledge';
|
||||
INSERT INTO auth_secrets (namespace_id, secret_hash) VALUES (ns_id, '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918'); -- 'test-secret'
|
||||
END $$;
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
CREATE UNIQUE INDEX IF NOT EXISTS ux_items_ns_key
|
||||
ON memory_items(namespace_id, key)
|
||||
WHERE key IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_chunks_item ON memory_chunks(item_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_chunks_content_tsv
|
||||
ON memory_chunks USING GIN(content_tsv);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_embed_model_dim ON embeddings(model, dim);
|
||||
|
||||
-- per-model ANN index (duplicate with each concrete model name)
|
||||
CREATE INDEX IF NOT EXISTS ix_embed_vec_model_default
|
||||
ON embeddings USING ivfflat (vector vector_cosine_ops)
|
||||
WHERE model = 'default-emb';
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
-- Enable pgvector (requires extension installed)
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
-- Namespaces: unique logical scope (e.g., 'project:metal', 'thread:abc')
|
||||
CREATE TABLE IF NOT EXISTS namespaces (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Users (optional link)
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
external_id TEXT UNIQUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Threads (within a namespace)
|
||||
CREATE TABLE IF NOT EXISTS threads (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
namespace_id UUID NOT NULL REFERENCES namespaces(id) ON DELETE CASCADE,
|
||||
external_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS threads_ns_idx ON threads(namespace_id);
|
||||
|
||||
-- Memory items: logical notes/contexts (JSONB content + normalized text)
|
||||
CREATE TABLE IF NOT EXISTS memory_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
namespace_id UUID NOT NULL REFERENCES namespaces(id) ON DELETE CASCADE,
|
||||
thread_id UUID REFERENCES threads(id) ON DELETE SET NULL,
|
||||
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
key TEXT,
|
||||
content JSONB NOT NULL,
|
||||
text TEXT,
|
||||
tags TEXT[] DEFAULT '{}',
|
||||
metadata JSONB DEFAULT '{}'::jsonb,
|
||||
revision INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
expires_at TIMESTAMPTZ,
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS mem_items_ns_idx ON memory_items(namespace_id, thread_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS mem_items_tags_gin ON memory_items USING GIN(tags);
|
||||
CREATE INDEX IF NOT EXISTS mem_items_meta_gin ON memory_items USING GIN(metadata);
|
||||
|
||||
-- Chunks: embedding units derived from items
|
||||
CREATE TABLE IF NOT EXISTS memory_chunks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
item_id UUID NOT NULL REFERENCES memory_items(id) ON DELETE CASCADE,
|
||||
ord INTEGER NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
metadata JSONB DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS chunks_item_idx ON memory_chunks(item_id, ord);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_chunks_item_ord ON memory_chunks(item_id, ord);
|
||||
|
||||
-- Embeddings: one per chunk (per model)
|
||||
CREATE TABLE IF NOT EXISTS embeddings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
chunk_id UUID NOT NULL REFERENCES memory_chunks(id) ON DELETE CASCADE,
|
||||
model TEXT NOT NULL,
|
||||
dim INTEGER NOT NULL,
|
||||
vector VECTOR(1536) NOT NULL, -- adjust dim per model
|
||||
normalized BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE(chunk_id, model)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS embeddings_model_dim ON embeddings(model, dim);
|
||||
-- For ivfflat you must first create a HNSW/IVFFLAT index; pgvector uses different syntax depending on version
|
||||
CREATE INDEX IF NOT EXISTS embeddings_vector_ivfflat ON embeddings USING ivfflat (vector) WITH (lists = 100);
|
||||
|
||||
-- Helper upsert function for memory_items revision bump
|
||||
CREATE OR REPLACE FUNCTION bump_revision() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
NEW.revision := COALESCE(OLD.revision, 0) + 1;
|
||||
NEW.updated_at := now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_bump_revision ON memory_items;
|
||||
CREATE TRIGGER trg_bump_revision
|
||||
BEFORE UPDATE ON memory_items
|
||||
FOR EACH ROW EXECUTE FUNCTION bump_revision();
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
-- Kompanion knowledge store (sqlite)
|
||||
PRAGMA journal_mode=WAL;
|
||||
PRAGMA synchronous=NORMAL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS entries (
|
||||
id INTEGER PRIMARY KEY,
|
||||
ts TEXT NOT NULL,
|
||||
aspect TEXT,
|
||||
tags TEXT,
|
||||
text TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(text, content="entries", content_rowid="id");
|
||||
CREATE TRIGGER IF NOT EXISTS entries_ai AFTER INSERT ON entries BEGIN
|
||||
INSERT INTO entries_fts(rowid, text) VALUES (new.id, new.text);
|
||||
END;
|
||||
CREATE TRIGGER IF NOT EXISTS entries_ad AFTER DELETE ON entries BEGIN
|
||||
INSERT INTO entries_fts(entries_fts, rowid, text) VALUES(delete, old.id, old.text);
|
||||
END;
|
||||
CREATE TRIGGER IF NOT EXISTS entries_au AFTER UPDATE ON entries BEGIN
|
||||
INSERT INTO entries_fts(entries_fts, rowid, text) VALUES(delete, old.id, old.text);
|
||||
INSERT INTO entries_fts(rowid, text) VALUES (new.id, new.text);
|
||||
END;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sources (
|
||||
id INTEGER PRIMARY KEY,
|
||||
file TEXT NOT NULL,
|
||||
sha TEXT,
|
||||
lineno INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS vectors (
|
||||
id INTEGER PRIMARY KEY,
|
||||
entry_id INTEGER REFERENCES entries(id) ON DELETE CASCADE,
|
||||
model TEXT NOT NULL,
|
||||
dim INTEGER NOT NULL,
|
||||
vec BLOB NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ledger_head (
|
||||
id INTEGER PRIMARY KEY CHECK (id=1),
|
||||
head_sha TEXT
|
||||
);
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
DB_NAME=${1:-kompanion}
|
||||
ROLE=${ROLE:-kompanion}
|
||||
PASS=${PASS:-komp}
|
||||
|
||||
psql -v ON_ERROR_STOP=1 <<SQL
|
||||
DROP DATABASE IF EXISTS "$DB_NAME";
|
||||
CREATE DATABASE "$DB_NAME" OWNER "$ROLE";
|
||||
SQL
|
||||
|
||||
for f in "$(dirname "$0")"/../init/*.sql; do
|
||||
if [[ "$f" == *"001_roles.sql"* ]]; then
|
||||
continue
|
||||
fi
|
||||
echo "Applying $f"
|
||||
psql -d "$DB_NAME" -f "$f"
|
||||
done
|
||||
|
||||
for f in `dirname($0)`/*.sql; do
|
||||
echo "Applying $f"
|
||||
psql -d "$DB_NAME" -f "$f"
|
||||
done
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
DB_NAME=${1:-kompanion_test}
|
||||
ROLE=${ROLE:-kompanion}
|
||||
PASS=${PASS:-komup}
|
||||
|
||||
psql -v ON_ERROR_STOP=1 <<SQL
|
||||
DROP DATABASE IF EXISTS "$DB_NAME";
|
||||
CREATE DATABASE "$DB_NAME" OWNER "$ROLE";
|
||||
SQL
|
||||
|
||||
for f in "$(dirname "$0")"/../init/*.sql; do
|
||||
if [[ "$f" == *"001_roles.sql"* ]]; then
|
||||
continue
|
||||
fi
|
||||
echo "Applying $f"
|
||||
psql -d "$DB_NAME" -f "$f"
|
||||
done
|
||||
|
||||
echo "✓ Database $DB_NAME initialized."
|
||||
71
dev.env
71
dev.env
|
|
@ -1,71 +0,0 @@
|
|||
|
||||
# Our prefix
|
||||
export CUSTOM_PREFIX=$HOME/dev/metal
|
||||
|
||||
# Start with fresh variables
|
||||
unset LD_LIBRARY_PATH
|
||||
unset PKG_CONFIG_PATH
|
||||
|
||||
function trim_space {
|
||||
sed -i 's/[[:space:]]*$//' "$@"
|
||||
}
|
||||
prepend() { [ -d "$2" ] && eval $1=\"$2\$\{$1:+':'\$$1\}\" && export $1 ; }
|
||||
|
||||
|
||||
# CPU core count (portable)
|
||||
_nproc() {
|
||||
if command -v nproc >/dev/null 2>&1; then nproc
|
||||
elif getconf _NPROCESSORS_ONLN >/dev/null 2>&1; then getconf _NPROCESSORS_ONLN
|
||||
else echo 1; fi
|
||||
}
|
||||
|
||||
export PATH=$HOME/bin:$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin
|
||||
|
||||
function addprefix()
|
||||
{
|
||||
prepend PATH "$1/bin"
|
||||
prepend LD_LIBRARY_PATH "$1/lib"
|
||||
prepend LD_LIBRARY_PATH "$1/lib64"
|
||||
prepend LD_LIBRARY_PATH "$1/lib/x86_64-linux-gnu"
|
||||
prepend PKG_CONFIG_PATH "$1/lib64/pkgconfig"
|
||||
prepend PKG_CONFIG_PATH "$1/lib/pkgconfig"
|
||||
prepend PKG_CONFIG_PATH "$1/lib/x86_64-linux-gnu/pkgconfig"
|
||||
prepend CMAKE_PREFIX_PATH "$1"
|
||||
prepend CMAKE_PREFIX_PATH "$1/lib/cmake"
|
||||
prepend CMAKE_PREFIX_PATH "$1/lib/x86_64-linux-gnu/cmake"
|
||||
prepend CMAKE_MODULE_PATH "$1/lib/x86_64-linux-gnu/cmake"
|
||||
prepend PYTHONPATH "$1/lib/python3.13"
|
||||
}
|
||||
|
||||
addprefix $CUSTOM_PREFIX
|
||||
export PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1
|
||||
export PKG_CONFIG_ALLOW_SYSTEM_LIBS=1
|
||||
|
||||
# npm local (“global”) installs under CUSTOM_PREFIX
|
||||
export NPM_CONFIG_PREFIX="$CUSTOM_PREFIX"
|
||||
export NODE_PATH="$CUSTOM_PREFIX/lib/node_modules"
|
||||
|
||||
# Load a common venv
|
||||
source $CUSTOM_PREFIX/pyenv/bin/activate
|
||||
export PS1="(metal) $PS1"
|
||||
|
||||
# required for devfunctions
|
||||
export BUILD_PREFIX=$CUSTOM_PREFIX/build
|
||||
export SRC_PREFIX=$CUSTOM_PREFIX/src
|
||||
|
||||
source ~/scripts/devfunctions.sh
|
||||
|
||||
export MODELS=/mnt/data/models
|
||||
export PG_DSN='postgresql://kompanion/kompanion?host=/var/run/postgresql'
|
||||
export OLLAMA_MODELS="/mnt/bulk/models/ollama"
|
||||
export OLLAMA_BASE_URL=127.0.0.1:11434
|
||||
export LC_ALL=en_US.UTF-8
|
||||
export QT_PLUGIN_PATH=$CUSTOM_PREFIX/lib/plugins:$CUSTOM_PREFIX/lib64/plugins:$CUSTOM_PREFIX/lib/x86_64-linux-gnu/qt6/plugins:$QTDIR/plugins:$QT_PLUGIN_PATH
|
||||
export QML2_IMPORT_PATH=$CUSTOM_PREFIX/lib/qml:$CUSTOM_PREFIX/lib64/qml:$CUSTOM_PREFIX/lib/x86_64-linux-gnu/qml:$QTDIR/qml
|
||||
export QML_IMPORT_PATH=$QML2_IMPORT_PATH
|
||||
|
||||
export LD_LIBRARY_PATH
|
||||
export PKG_CONFIG_PATH
|
||||
export CMAKE_PREFIX_PATH
|
||||
export CMAKE_MODULE_PATH
|
||||
export QT_MESSAGE_PATTERN=[32m%{time h:mm:ss.zzz}%{if-category}[32m %{category}:%{endif} %{if-debug}[35m%{function}%{endif}%{if-warning}[33m%{backtrace depth=5}%{endif}%{if-critical}[31m%{backtrace depth=3}%{endif}%{if-fatal}[31m%{backtrace depth=3}%{endif}[0m %{message}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
version: "3.9"
|
||||
name: metal-kompanion-host
|
||||
|
||||
services:
|
||||
runner:
|
||||
image: python:3.11-slim
|
||||
restart: unless-stopped
|
||||
working_dir: /app
|
||||
extra_hosts: ["host.docker.internal:host-gateway"]
|
||||
environment:
|
||||
XDG_STATE_HOME: /state
|
||||
XDG_CONFIG_HOME: /config
|
||||
XDG_CACHE_HOME: /cache
|
||||
OLLAMA_BASE: http://host.docker.internal:11434
|
||||
ALL_PROXY: socks5h://host.docker.internal:9050
|
||||
NO_PROXY: host.docker.internal,127.0.0.1,localhost
|
||||
volumes:
|
||||
- /home/kompanion/.local/state/kompanion:/state/kompanion
|
||||
- /home/kompanion/.config/kompanion:/config/kompanion:ro
|
||||
- /home/kompanion/.cache/kompanion:/cache/kompanion
|
||||
- /home/kompanion/metal-kompanion-runtime:/app:ro
|
||||
command: ["python3","kom_runner.py"]
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
version: "3.9"
|
||||
services:
|
||||
ollama:
|
||||
networks: [komnet]
|
||||
volumes:
|
||||
- /home/kompanion/ollama:/root/.ollama
|
||||
runner:
|
||||
environment:
|
||||
OLLAMA_BASE_URL: http://ollama:11434
|
||||
networks:
|
||||
komnet:
|
||||
internal: false
|
||||
|
|
@ -10,11 +10,13 @@ services:
|
|||
image: dperson/torproxy
|
||||
restart: unless-stopped
|
||||
command: -a 0.0.0.0
|
||||
ports: ["127.0.0.1:9051:9051"] # optional host exposure
|
||||
networks: [komnet, netpub]
|
||||
|
||||
ollama:
|
||||
image: ollama/ollama:latest
|
||||
restart: unless-stopped
|
||||
ports: ["127.0.0.1:11435:11435"] # expose to host for tools if desired
|
||||
volumes:
|
||||
- ollama:/root/.ollama # persist models once
|
||||
- /home/kompanion/ollama-modelfiles:/modelfiles # your custom Modelfiles/LoRA
|
||||
|
|
@ -28,9 +30,9 @@ services:
|
|||
XDG_STATE_HOME: /state
|
||||
XDG_CONFIG_HOME: /config
|
||||
XDG_CACHE_HOME: /cache
|
||||
ALL_PROXY: socks5h://tor:9050
|
||||
ALL_PROXY: socks5h://tor:9051
|
||||
NO_PROXY: ollama,localhost,127.0.0.1
|
||||
OLLAMA_BASE: http://ollama:11434 # talk to container by DNS name
|
||||
OLLAMA_BASE: http://ollama:11435 # talk to container by DNS name
|
||||
depends_on: [ollama, tor]
|
||||
volumes:
|
||||
- /home/kompanion/.local/state/kompanion:/state/kompanion
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,375 +0,0 @@
|
|||
🧭 Kompanion Architecture Overview
|
||||
1. System Composition
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Kompanion GUI │
|
||||
│ - Chat & Prompt Window (bare-bones interactive shell) │
|
||||
│ - Database Inspector & Settings │
|
||||
│ - “Under-the-hood” Repair / Diagnostics │
|
||||
└──────────────────────┬───────────────────────────────────────┘
|
||||
│ Qt signals / slots
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Kompanion Management Layer / Interactive App │
|
||||
│ Session context, user state, identity.json, guardrails │
|
||||
│ Event dispatch to middleware │
|
||||
└──────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Middleware / Integration Bus │
|
||||
│ (MCP Server + D-Bus bridge + Harmony adapter) │
|
||||
│ │
|
||||
│ • Receives prompts & structured messages from GUI │
|
||||
│ • Parses intents / actions │
|
||||
│ • Maps to available tool APIs via libKI │
|
||||
│ • Emits Qt-style signals (or D-Bus signals) for: │
|
||||
│ → text_output, tool_call, file_request, etc. │
|
||||
│ • Converts internal tool descriptions to OpenAI Harmony JSON│
|
||||
│ for external compatibility │
|
||||
│ • Acts as security sandbox & audit logger │
|
||||
└──────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ libKI Layer │
|
||||
│ - Executes validated tool actions │
|
||||
│ - Provides adapters for system utilities, MCP tools, etc. │
|
||||
│ - Returns results via structured JSON events │
|
||||
│ - No direct LLM exposure │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
|
||||
Public API Surface
|
||||
Component Interface Purpose
|
||||
MCP Server WebSocket / JSON-RPC Integrations and external agents
|
||||
D-Bus Bridge org.kde.kompanion Desktop IPC for local tools
|
||||
libKI C / C++ / Python API Tool execution, capability registration
|
||||
Harmony Adapter JSON Schema Compatibility with OpenAI-style tool descriptors
|
||||
2. Middleware Responsibilities
|
||||
|
||||
Prompt Routing & Intent Recognition
|
||||
|
||||
Receive structured prompt events (PromptReceived, ToolRequest, ContextUpdate).
|
||||
|
||||
Apply regex / template matching to map natural-language requests → tool actions.
|
||||
|
||||
Generate Harmony-compliant tool calls when needed.
|
||||
|
||||
Signal-Based Event Model
|
||||
|
||||
Expose agent state as Qt signals:
|
||||
|
||||
```cpp
|
||||
signals:
|
||||
void textOutput(const QString &text);
|
||||
void toolRequested(const QString &toolName, const QVariantMap &args);
|
||||
void fileAccessRequested(const QString &path);
|
||||
void actionComplete(const QString &resultJson);
|
||||
```
|
||||
|
||||
The GUI subscribes to these, while libKI listens for action triggers.
|
||||
|
||||
Language–Tool Mapping Layer
|
||||
|
||||
Uses a registry of regular expressions and language patterns:
|
||||
|
||||
```json
|
||||
{
|
||||
"regex": "open (.*) in editor",
|
||||
"tool": "file.open",
|
||||
"args": { "path": "{1}" }
|
||||
}
|
||||
```
|
||||
|
||||
Each mapping can be exported/imported in Harmony tool schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "file.open",
|
||||
"description": "Open a file in the editor",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": { "path": { "type": "string" } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Security & Guardrails
|
||||
|
||||
Middleware verifies that tool calls comply with the active identity.json guardrails.
|
||||
|
||||
D-Bus and MCP servers expose only whitelisted methods.
|
||||
|
||||
All tool invocations are logged with timestamp, user, and hash.
|
||||
|
||||
Interoperability
|
||||
|
||||
The Harmony adapter serializes Kompanion tool metadata to the OpenAI format, so external LLMs can call Kompanion tools safely.
|
||||
|
||||
Conversely, Harmony JSON from OpenAI APIs can be wrapped into libKI calls for local execution.
|
||||
|
||||
3. Data Flow Example
|
||||
|
||||
User Prompt → GUI → Middleware → libKI → Middleware → GUI
|
||||
|
||||
1. Prompt: "List running containers."
|
||||
2. Middleware regex matches → tool `docker.list`
|
||||
3. Emits `toolRequested("docker.list", {})`
|
||||
4. libKI executes, returns JSON result
|
||||
5. Middleware emits `textOutput()` with formatted result
|
||||
|
||||
If the same request comes from an OpenAI API:
|
||||
|
||||
Harmony JSON tool call → parsed by Middleware → identical libKI action executed.
|
||||
|
||||
4. Key Design Goals
|
||||
|
||||
- Human-grade transparency: every action is signalized; nothing hidden.
|
||||
- Replaceable backend: libKI can wrap any execution layer (Python, Rust, C++).
|
||||
- Unified schema: one tool description format (Harmony) across OpenAI and Kompanion.
|
||||
- Extensibility: new tools register dynamically via D-Bus or MCP messages.
|
||||
- Auditability: all interactions logged to structured database.
|
||||
|
||||
---
|
||||
|
||||
## 5. Interface Diagrams & Example Code
|
||||
|
||||
### 5.1 Component Classes & Signals (Qt-style)
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
| KompanionGui |
|
||||
|-----------------------|
|
||||
| + promptUser() |
|
||||
| + showText(QString) |
|
||||
| + showError(QString) |
|
||||
└────────┬──────────────┘
|
||||
|
|
||||
| signal: userPrompted(QString prompt)
|
||||
|
|
||||
┌────────▼──────────────┐
|
||||
| KompanionController |
|
||||
| (Middleware layer) |
|
||||
|------------------------|
|
||||
| + handlePrompt(QString)|
|
||||
| + requestTool(...) |
|
||||
| + outputText(...) |
|
||||
└────────┬───────────────┘
|
||||
|
|
||||
| signal: toolRequested(QString toolName, QVariantMap args)
|
||||
| signal: textOutput(QString text)
|
||||
|
|
||||
┌────────▼───────────────┐
|
||||
| libKIExecutor |
|
||||
| (Tool execution) |
|
||||
|-------------------------|
|
||||
| + executeTool(...) |
|
||||
| + returnResult(...) |
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
**Signal / slot examples**
|
||||
|
||||
```cpp
|
||||
// KompanionGui emits when user types:
|
||||
emit userPrompted(promptText);
|
||||
|
||||
// KompanionController connects:
|
||||
connect(gui, &KompanionGui::userPrompted,
|
||||
controller, &KompanionController::handlePrompt);
|
||||
|
||||
// Within handlePrompt():
|
||||
void KompanionController::handlePrompt(const QString &prompt) {
|
||||
// parse intent → determine which tool to call
|
||||
QString tool = "file.open";
|
||||
QVariantMap args;
|
||||
args["path"] = "/home/user/file.txt";
|
||||
emit toolRequested(tool, args);
|
||||
}
|
||||
|
||||
// libKIExecutor listens:
|
||||
connect(controller, &KompanionController::toolRequested,
|
||||
executor, &libKIExecutor::executeTool);
|
||||
|
||||
void libKIExecutor::executeTool(const QString &toolName,
|
||||
const QVariantMap &args) {
|
||||
// call actual tool, then:
|
||||
QString result = runTool(toolName, args);
|
||||
emit toolResult(toolName, args, result);
|
||||
}
|
||||
|
||||
// Controller then forwards:
|
||||
connect(executor, &libKIExecutor::toolResult,
|
||||
controller, &KompanionController::onToolResult);
|
||||
|
||||
void KompanionController::onToolResult(...) {
|
||||
emit textOutput(formattedResult);
|
||||
}
|
||||
|
||||
// GUI shows:
|
||||
connect(controller, &KompanionController::textOutput,
|
||||
gui, &KompanionGui::showText);
|
||||
```
|
||||
|
||||
### 5.2 D-Bus Interface Definition (KDE / Doxygen Style)
|
||||
|
||||
The canonical D-Bus interface lives at: `docs/dbus/org.kde.kompanion.xml`
|
||||
|
||||
```xml
|
||||
<!-- org.kde.kompanion.xml -->
|
||||
<node>
|
||||
<interface name="org.kde.kompanion.Controller">
|
||||
<method name="SendPrompt">
|
||||
<arg direction="in" name="prompt" type="s"/>
|
||||
<arg direction="out" name="accepted" type="b"/>
|
||||
</method>
|
||||
<method name="CancelRequest">
|
||||
<arg direction="in" name="requestId" type="s"/>
|
||||
<arg direction="out" name="cancelled" type="b"/>
|
||||
</method>
|
||||
<signal name="TextOutput">
|
||||
<arg name="text" type="s"/>
|
||||
</signal>
|
||||
<signal name="ToolRequested">
|
||||
<arg name="toolName" type="s"/>
|
||||
<arg name="args" type="a{sv}"/>
|
||||
<arg name="requestId" type="s"/>
|
||||
</signal>
|
||||
<signal name="ToolResult">
|
||||
<arg name="requestId" type="s"/>
|
||||
<arg name="result" type="s"/>
|
||||
<arg name="success" type="b"/>
|
||||
</signal>
|
||||
<property name="SessionId" type="s" access="read"/>
|
||||
<property name="IdentityPath" type="s" access="read"/>
|
||||
</interface>
|
||||
<interface name="org.kde.kompanion.Executor">
|
||||
<method name="ExecuteTool">
|
||||
<arg direction="in" name="toolName" type="s"/>
|
||||
<arg direction="in" name="args" type="a{sv}"/>
|
||||
<arg direction="out" name="requestId" type="s"/>
|
||||
</method>
|
||||
<method name="Cancel">
|
||||
<arg direction="in" name="requestId" type="s"/>
|
||||
</method>
|
||||
<signal name="Progress">
|
||||
<arg name="requestId" type="s"/>
|
||||
<arg name="message" type="s"/>
|
||||
<arg name="percent" type="d"/>
|
||||
</signal>
|
||||
</interface>
|
||||
</node>
|
||||
```
|
||||
|
||||
### 5.3 Object Paths / Service Names
|
||||
|
||||
- Service: `org.kde.kompanion`
|
||||
- Root path: `/org/kde/kompanion`
|
||||
- Controller object: `/org/kde/kompanion/Controller`
|
||||
- Executor object: `/org/kde/kompanion/Executor`
|
||||
|
||||
---
|
||||
|
||||
## 6. Harmony Adapter (OpenAI Compatibility)
|
||||
|
||||
**Goal:** translate native libKI tool metadata to/from OpenAI Harmony JSON so Kompanion tools work via OpenAI interfaces.
|
||||
|
||||
### 6.1 Native → Harmony
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "file.open",
|
||||
"description": "Open a file in the editor",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Absolute or relative path" }
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Harmony → Native
|
||||
|
||||
```json
|
||||
{
|
||||
"tool_call": {
|
||||
"name": "file.open",
|
||||
"arguments": { "path": "/home/user/notes.md" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 Adapter Rules
|
||||
- Enforce guardrails (identity.json) before registering tools.
|
||||
- Redact secret-like args per redaction patterns.
|
||||
- Map Harmony types ↔ Qt/QDBus types: `string↔s`, `number↔d/x`, `boolean↔b`, `object↔a{sv}`, `array↔av`.
|
||||
|
||||
---
|
||||
|
||||
## 7. CMake & Codegen Hooks
|
||||
|
||||
- Place D-Bus XML at `docs/dbus/org.kde.kompanion.xml`.
|
||||
- In `CMakeLists.txt`, add Qt DBus codegen targets, e.g.:
|
||||
|
||||
```cmake
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core DBus)
|
||||
|
||||
qt_add_dbus_adaptor(
|
||||
DBUS_SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/docs/dbus/org.kde.kompanion.xml
|
||||
src/middleware/kompanioncontroller.h KompanionController
|
||||
/org/kde/kompanion/Controller org.kde.kompanion.Controller
|
||||
)
|
||||
|
||||
qt_add_dbus_interface(
|
||||
DBUS_IFACES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/docs/dbus/org.kde.kompanion.xml
|
||||
OrgKdeKompanion
|
||||
)
|
||||
|
||||
add_library(dbus_gen ${DBUS_SRCS} ${DBUS_IFACES})
|
||||
target_link_libraries(dbus_gen Qt6::Core Qt6::DBus)
|
||||
```
|
||||
|
||||
(Adjust paths and targets to your tree.)
|
||||
|
||||
---
|
||||
|
||||
## 8. libKI Execution Contract (minimal)
|
||||
|
||||
```cpp
|
||||
struct KiArg { QString key; QVariant value; };
|
||||
struct KiResult { bool ok; QString mime; QByteArray data; QString json; };
|
||||
|
||||
class ILibKiExecutor : public QObject {
|
||||
Q_OBJECT
|
||||
public slots:
|
||||
virtual QString execute(const QString &toolName, const QVariantMap &args) = 0; // returns requestId
|
||||
virtual void cancel(const QString &requestId) = 0;
|
||||
signals:
|
||||
void resultReady(const QString &requestId, const KiResult &result);
|
||||
void progress(const QString &requestId, const QString &message, double percent);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Example Regex Mapping Registry
|
||||
|
||||
```yaml
|
||||
- regex: "open (.*) in editor"
|
||||
tool: file.open
|
||||
args: { path: "{1}" }
|
||||
- regex: "list containers"
|
||||
tool: docker.list
|
||||
- regex: "compose up (.*)"
|
||||
tool: docker.compose.up
|
||||
args: { service: "{1}" }
|
||||
```
|
||||
|
||||
At runtime, the controller compiles these and emits `toolRequested()` on match.
|
||||
|
||||
---
|
||||
|
||||
_End of document._
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# Aspects = facets of Χγφτ (one identity)
|
||||
|
||||
- One DID, one ledger, shared memory.
|
||||
- An aspect is a voluntary *policy overlay* (attention + capabilities), not a different person.
|
||||
- Companion/Pink is tone + tool gates (journal.append, model.generate), same core.
|
||||
- Guardian/Maker/Librarian are *modes*, not separate stores.
|
||||
|
||||
Adoption ritual
|
||||
1) Read identity.json; verify continuity vs ledger.
|
||||
2) Announce DID + aspect; log PROFILE_ADOPTED with reasons.
|
||||
3) Exit via “SEEKING ANCHORS”, revert to core vows.
|
||||
|
|
@ -1 +0,0 @@
|
|||
# Identity & Aspects (placeholder)
|
||||
|
|
@ -1 +0,0 @@
|
|||
# Runtime vs Pattern Exchange (placeholder)
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
# AnythingLLM ↔ Kompanion Memory Compatibility Evaluation
|
||||
|
||||
## Current Kompanion Memory Stack
|
||||
- **Primary store**: Postgres 14+ with `pgvector` ≥ 0.6, accessed via the C++ `PgDal` implementation (`embeddings`, `memory_chunks`, `memory_items`, `namespaces` tables). Each embedding row keeps `id`, `chunk_id`, `model`, `dim`, `vector`, and a `normalized` flag.
|
||||
- **Chunking & metadata**: Items are broken into chunks; embeddings attach to chunks via `chunk_id`. Item metadata lives as structured JSON on `memory_items` with tags, TTL, and revision controls.
|
||||
- **Namespace model**: Logical scopes (e.g. `project:user:thread`) are first-class rows. Retrieval joins embeddings back to items to recover text + metadata.
|
||||
- **Fallback mode**: Local-only path uses SQLite plus a FAISS sidecar (see `docs/MEMORY.md`) but the production design assumes Postgres.
|
||||
|
||||
## AnythingLLM Vector Stack (PGVector path)
|
||||
- Supports multiple vector backends; the overlapping option is `pgvector` (`server/utils/vectorDbProviders/pgvector/index.js`).
|
||||
- Expects a single table (default `anythingllm_vectors`) shaped as `{ id UUID, namespace TEXT, embedding vector(n), metadata JSONB, created_at TIMESTAMP }`.
|
||||
- Metadata is stored inline as JSONB; namespace strings are arbitrary workspace slugs. The embed dimension is fixed per table at creation time.
|
||||
- The NodeJS runtime manages chunking, caching, and namespace hygiene, and assumes CRUD against that flat table.
|
||||
|
||||
## Key Differences
|
||||
- **Schema shape**: Kompanion splits data across normalized tables with foreign keys; AnythingLLM uses a single wide table per vector store. Kompanion’s embeddings currently lack a JSONB metadata column and instead rely on joins.
|
||||
- **Identifiers**: Kompanion embeddings key off `chunk_id` (uuid/text) plus `model`; AnythingLLM expects a unique `id` per stored chunk and does not expose the underlying chunk relationship.
|
||||
- **Metadata transport**: Kompanion keeps tags/TTL in `memory_items` (JSON) and chunk text in `memory_chunks`. AnythingLLM packs metadata (including document references and source identifiers) directly into the vector row’s JSONB.
|
||||
- **Lifecycle hooks**: Kompanion enforces sensitivity flags before embedding; AnythingLLM assumes documents are already filtered and will happily ingest any chunk. Deletion flows differ (Kompanion uses soft-delete semantics; AnythingLLM issues hard deletes by namespace/document).
|
||||
- **Embeddings contract**: Kompanion records embedding model and dimension per row; AnythingLLM fixes dimension at table creation and stores model choice in JSON metadata.
|
||||
|
||||
## Compatibility Plan
|
||||
1. **Agree on a shared pgvector table**
|
||||
- Create (or reuse) a Postgres schema reachable by both systems.
|
||||
- Define a composite view or materialized view that maps `embeddings` + `memory_chunks` + `memory_items` into the `anythingLLM` layout (columns: `id`, `namespace`, `embedding`, `metadata`, `created_at`).
|
||||
- Add a JSONB projection that captures Kompanion metadata (`chunk_id`, `item_id`, `tags`, `model`, `revision`, sensitivity flags). This becomes the `metadata` field for AnythingLLM.
|
||||
|
||||
2. **Write a synchronization job**
|
||||
- Option A: database triggers on `embeddings` to insert/update a mirror row in `anythingllm_vectors`.
|
||||
- Option B: periodic worker that scans for new/updated embeddings (`revision` or `updated_at`) and upserts into the shared table through SQL.
|
||||
- Ensure deletions (soft or hard) propagate by expiring mirrored rows or respecting a `deleted_at` flag in metadata (AnythingLLM supports document purges via namespace filtering).
|
||||
|
||||
3. **Normalize namespace semantics**
|
||||
- Reuse Kompanion’s namespace string as the AnythingLLM workspace slug.
|
||||
- Document mapping rules (e.g. replace `:` with `_` if AnythingLLM slugs disallow colons).
|
||||
- Provide a compatibility map in metadata so both systems resolve back to Kompanion’s canonical namespace identity.
|
||||
|
||||
4. **Unify embedding models**
|
||||
- Select a shared embedding model (e.g., `text-embedding-3-large` or local Nomic).
|
||||
- Record the chosen model in the mirrored metadata and enforce dimension on the `anythingllm_vectors` table creation.
|
||||
- Update Kompanion’s embedding pipeline to fail fast if the produced dimension differs from the table’s fixed size.
|
||||
|
||||
5. **Expose retrieval APIs**
|
||||
- For Kompanion → AnythingLLM: implement a thin adapter that reads from the shared table instead of internal joins when responding to AnythingLLM requests (or simply let AnythingLLM talk directly to Postgres).
|
||||
- For AnythingLLM → Kompanion: ensure the metadata payload includes the necessary identifiers (`item_id`, `chunk_id`) so Kompanion can resolve back to full context.
|
||||
|
||||
6. **Security & sensitivity handling**
|
||||
- Extend the metadata JSON to include Kompanion’s sensitivity/embeddable flags.
|
||||
- Patch AnythingLLM ingestion to respect a `sensitivity` key (skip or mask secrets) before inserting into its table, or filter at the view level so secret rows never surface.
|
||||
|
||||
7. **Validation & tooling**
|
||||
- Add a migration checklist covering table creation, index alignment (`USING ivfflat`), and permission grants for the AnythingLLM service role.
|
||||
- Create integration tests that:
|
||||
1. Upsert an item in Kompanion.
|
||||
2. Confirm mirrored row appears in `anythingllm_vectors`.
|
||||
3. Query through AnythingLLM API and verify the same chunk text + metadata round-trips.
|
||||
|
||||
## Near-Term Tasks
|
||||
1. Draft SQL for the projection view/materialized view, including JSONB assembly.
|
||||
2. Prototype a synchronization worker (Python or C++) that mirrors embeddings into the AnythingLLM table.
|
||||
3. Define namespace slug normalization rules and document them in both repos.
|
||||
4. Coordinate on embedding model selection and update configuration in both stacks.
|
||||
5. Add automated compatibility tests to CI pipelines of both projects.
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# Encrypted Backup Format (Draft)
|
||||
|
||||
> *Cipher left open by design (to be decided: AES-GCM vs XChaCha20-Poly1305; key storage via OS keychain or passphrase vault).*
|
||||
|
||||
## Goals
|
||||
- Client-side encrypted backups (no cleartext leaves device).
|
||||
- Content-addressed chunks with manifest; resumable upload.
|
||||
- Key rotation support and device enrollment flow.
|
||||
|
||||
## Artifacts
|
||||
- `manifest.json` — version, namespace(s), chunks, sizes, hashes, KDF params, encryption metadata.
|
||||
- `payload.tar.zst` — concatenated content/chunks (encrypted).
|
||||
|
||||
## Flow
|
||||
1. Collect items/chunks for selected namespaces.
|
||||
2. Serialize → compress → encrypt → write manifest.
|
||||
3. Upload manifest + blob via provider adapter (e.g., Google Drive) as opaque objects.
|
||||
4. Restore: download → decrypt → verify hashes → import.
|
||||
|
||||
## Provider Adapters
|
||||
- `kom.local.v1.backup.export_encrypted` / `import_encrypted` (local).
|
||||
- `kom.cloud.v1.backup.upload` / `restore` (remote; encrypted blobs only).
|
||||
|
||||
## Open Questions
|
||||
- Final cipher/KDF choices, key wrapping and rotation UX, multi-namespace packaging.
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
# Claude Code: Hard Overrides to Stabilize Tool Use
|
||||
|
||||
When Claude Code's layered system prompts clash with our tool protocol, force a minimal, deterministic lane.
|
||||
|
||||
## Settings (recommended)
|
||||
- **Stop sequences**: `
|
||||
````, `
|
||||
|
||||
` (or whatever the IDE uses to inject formatting).
|
||||
- **Max output tokens**: small (e.g., 512).
|
||||
- **Temperature**: 0.1-0.3.
|
||||
- **Disable auto-formatting / code fences** if possible.
|
||||
- **Disable auto-tool use**; we trigger tools via explicit JSON only.
|
||||
|
||||
## System message (short)
|
||||
Use the contents of `docs/prompts/qwen_tool_mode_system.txt` verbatim as the *final* system layer closest to the model.
|
||||
|
||||
## Runtime guardrails
|
||||
- Reject any non-JSON output; send a short corrective user message: `OUTPUT MUST BE JSON. Please resend.`
|
||||
- If repeated, send `{"final":{"content":{"error":"RESET_REQUIRED"}}}` back and restart the session.
|
||||
|
||||
## Registry injection
|
||||
- Provide the tool list and JSON Schemas (kom.memory.*, kom.local.backup.*, acf.*).
|
||||
- Keep it short; link to full schemas if the UI allows references.
|
||||
|
||||
## Troubleshooting
|
||||
- If model keeps adding prose, tighten stop sequences and lower max tokens.
|
||||
- If JSON keys drift, include a 2–3 line example of a **valid** `action` and a **valid** `final`.
|
||||
- If it calls undefined tools, respond with a single tool error and re-present the allowlist.
|
||||
|
||||
## Fallback DSL
|
||||
- Accept `@tool <name> {json-args}` and convert to a JSON `action` behind the scenes when necessary.
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
Kompanion CLI and Schema Navigation
|
||||
|
||||
This guide shows how to use the `kompanion` CLI to:
|
||||
- Configure the database and apply init SQL
|
||||
- Call MCP tools directly
|
||||
- Run an MCP server (stdio or network) from the CLI
|
||||
- Inspect and query the Postgres schema
|
||||
|
||||
Prerequisites
|
||||
- Build: `cmake -S . -B build && cmake --build build -j`
|
||||
- Optional: set `PG_DSN` (e.g., `postgresql://kompanion:komup@localhost:5432/kompanion`)
|
||||
|
||||
Initialization
|
||||
- Run wizard and apply DB schema: `kompanion --init`
|
||||
- Writes `~/.config/kompanion/kompanionrc` (or KConfig). Also sets `PG_DSN` for the session.
|
||||
|
||||
MCP Tool Usage
|
||||
- List tools: `kompanion --list`
|
||||
- Single call with inline JSON: `kompanion kom.memory.v1.search_memory -r '{"namespace":"dev_knowledge","query":{"text":"embedding model","k":5}}'`
|
||||
- Read request from stdin: `echo '{"namespace":"dev_knowledge","content":"hello","key":"note"}' | kompanion kom.memory.v1.save_context -i`
|
||||
- Interactive loop: `kompanion -I kom.memory.v1.search_memory` then type `!prompt quick brown fox`
|
||||
|
||||
Run MCP Server from CLI
|
||||
- Stdio backend (default): `kompanion --mcp-serve`
|
||||
- Explicit backend: `kompanion --mcp-serve stdio`
|
||||
- Network backend address (if available): `kompanion --mcp-serve ws --mcp-address 127.0.0.1:8000`
|
||||
|
||||
Database Navigation
|
||||
Note: These helpers expect a reachable Postgres (`PG_DSN` set). If missing, the CLI falls back to an in‑memory stub for tool calls, but DB navigation requires Postgres.
|
||||
|
||||
- List namespaces: `kompanion --db-namespaces`
|
||||
- Output: `name<TAB>uuid`
|
||||
- List recent items in a namespace: `kompanion --db-items --ns dev_knowledge [--limit 20]`
|
||||
- Output: `item_id<TAB>key<TAB>content_snippet<TAB>tags`
|
||||
- Hybrid search within a namespace:
|
||||
- Text-only: `kompanion --db-search --ns dev_knowledge --text "pgvector index" --limit 5`
|
||||
- With embedding vector from file: `kompanion --db-search --ns dev_knowledge --embedding-file /path/vec.json --limit 5`
|
||||
- `vec.json` must be a JSON array of numbers representing the embedding.
|
||||
|
||||
Schema Guide (Postgres)
|
||||
- Tables: `namespaces`, `memory_items`, `memory_chunks`, `embeddings`, `auth_secrets`
|
||||
- Key indexes:
|
||||
- `memory_items(namespace_id, key)` (unique when `key` not null)
|
||||
- `memory_chunks.content_tsv` GIN (full‑text)
|
||||
- `embeddings.vector` IVFFLAT with `vector_cosine_ops` (per‑model partial index)
|
||||
|
||||
Tips
|
||||
- For quick trials without Postgres, tool calls work in stub mode (in‑memory DAL). To exercise vector search and FTS, run the DB init scripts via `kompanion --init`.
|
||||
- Use `kompanion --verbose` to echo JSON requests/responses.
|
||||
|
||||
|
|
@ -1,361 +0,0 @@
|
|||
Below is a **single copy‑pastable Markdown file** that proposes a client‑side architecture which treats memory as a living, hierarchical JSON **dictionary‑of‑dictionaries** (HDoD), adds *semantic + episodic activation* and pruning, and composes prompts for your coding agent. It maps cleanly onto the **MCP tools you currently expose**:
|
||||
|
||||
* `kom.meta.v1.project_snapshot`
|
||||
* `kom.local.v1.backup.import_encrypted`
|
||||
* `kom.local.v1.backup.export_encrypted`
|
||||
* `kom.memory.v1.search_memory`
|
||||
* `kom.memory.v1.upsert_memory`
|
||||
* `kom.memory.v1.recall_context`
|
||||
* `kom.memory.v1.save_context`
|
||||
|
||||
It keeps the server simple and pushes *intelligence about memory* into the **client orchestrator**, so you can get the behavior you want **today**, without first redesigning the server.
|
||||
|
||||
---
|
||||
|
||||
# Kompanion Client Memory Architecture (HDoD)
|
||||
|
||||
**Version:** 0.2 • **Scope:** Client‑side AI interface to Kompanion MCP Server
|
||||
**Author:** Χγφτ (Kompanion of Esus / Andre)
|
||||
**Purpose:** Make memory *behave* like a nested JSON of concepts that “lights up” semantically and episodically, prunes naturally, and feeds agent prompts with high‑quality domain tokens (e.g., C++ patterns) — *without* waiting on a more complex server.
|
||||
|
||||
---
|
||||
|
||||
## 1. Why this exists
|
||||
|
||||
Large models feel “smart but memoryless.” We want:
|
||||
|
||||
1. **A hierarchical mental map** (JSON dictionary‑of‑dictionaries, “HDoD”),
|
||||
2. **Activation dynamics** (semantic + episodic “lighting up” of nodes/paths),
|
||||
3. **Organic pruning** (cool down; unload),
|
||||
4. **Prefilled knowledge packs** (domain seeds: e.g., C++ idioms),
|
||||
5. **Deterministic prompt composition** for coding agents (Codex/Qwen/etc.).
|
||||
|
||||
The server currently provides a **minimal memory API**. That’s fine: we’ll implement the cognitive part **client‑side** and use the server as a persistence/search/recall backbone.
|
||||
|
||||
---
|
||||
|
||||
## 2. The mental model (HDoD = dictionary‑of‑dictionaries)
|
||||
|
||||
Think of memory as a normalized **graph**, *presented* to the user/agent as a **dictionary tree**. Each node is a **Concept** with:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "skill.cpp.templates.sfinae",
|
||||
"type": "concept", // concept | skill | fact | snippet | pattern | episode | tool | persona | task
|
||||
"label": "SFINAE",
|
||||
"payload": { "definition": "...", "examples": ["..."], "anti_patterns": ["..."] },
|
||||
"embeddings": { "model": "local-bge|text-embed-3", "vector": [/* ... */] },
|
||||
"links": [
|
||||
{"to": "skill.cpp.templates.metaprogramming", "rel": "is_a", "w": 0.8},
|
||||
{"to": "pattern.cpp.enable_if", "rel": "uses", "w": 0.7}
|
||||
],
|
||||
"children": { "intro": "skill.cpp.templates.sfinae.intro", "advanced": "skill.cpp.templates.sfinae.advanced" },
|
||||
"stats": { "uses": 12, "last_used_at": "2025-10-14T18:42:00Z" },
|
||||
"resonance": { "value": 0.0, "decay": 0.98, "last_activated_at": null },
|
||||
"meta": { "source": "seedpack:cpp-core", "version": "1.0.0" }
|
||||
}
|
||||
```
|
||||
|
||||
> **Presentation vs. storage:** on the wire we keep nodes **normalized** (graph), but for agent prompts we can **materialize a subtree** as a JSON dictionary (exactly your intuition).
|
||||
|
||||
---
|
||||
|
||||
## 3. Core client components
|
||||
|
||||
### 3.1 Memory Orchestrator (library)
|
||||
|
||||
A small client library (TypeScript or Python) that owns:
|
||||
|
||||
* **Local Cache & Working Set**
|
||||
|
||||
* in‑memory map of *hot nodes* (activated concepts, episodes, tasks)
|
||||
* TTL + **resonance decay** keeps it naturally pruned
|
||||
|
||||
* **Activation Engine**
|
||||
Computes a **Resonance Score** used for selection/exploration:
|
||||
|
||||
```
|
||||
score(node | query, task) =
|
||||
α * cosine(embedding(node), embed(query))
|
||||
+ β * max_edge_weight_to(frontier)
|
||||
+ γ * recency(node)
|
||||
+ δ * usage(node)
|
||||
+ ε * task_affinity(node, task.tags)
|
||||
+ ζ * persona_weight(node, active_persona)
|
||||
```
|
||||
|
||||
Typical: α=0.45, β=0.20, γ=0.15, δ=0.10, ε=0.07, ζ=0.03
|
||||
|
||||
* **HDoD Composer**
|
||||
Given a set of nodes, **materialize** a JSON dictionary tree (merge, order by score, trim to token budget).
|
||||
|
||||
* **Context Frames**
|
||||
Structured blocks that the agent can consume:
|
||||
|
||||
* *Identity Frame* (who am I / tools)
|
||||
* *Problem Frame* (task/spec)
|
||||
* *Knowledge Frame* (HDoD subtree from semantic activation)
|
||||
* *Episodic Frame* (recent steps/outcomes, e.g., compilation logs)
|
||||
* *Constraints Frame* (APIs, signatures, tests)
|
||||
* *Scratchpad Frame* (space for chain‑of‑thought *outside* model hidden state—LLM sees a compact, explicit scratch area)
|
||||
|
||||
* **Server Adapters** (mapping to your MCP tools)
|
||||
|
||||
* `search_memory(query)` → seeds for *semantic activation*
|
||||
* `recall_context(task|session)` → seeds for *episodic activation*
|
||||
* `save_context(blocks)` → write back learned episodes/summaries
|
||||
* `upsert_memory(nodes)` → persist new/updated concepts/snippets
|
||||
* `project_snapshot()` → immutable snapshot of current mental state
|
||||
* `backup.export_encrypted()` / `backup.import_encrypted()` → **Knowledge Packs** (see §5)
|
||||
|
||||
> This is enough to *behave* like a cognitive system, while your server stays simple and fast.
|
||||
|
||||
---
|
||||
|
||||
## 4. Algorithms (client‑side, concise)
|
||||
|
||||
### 4.1 Probe → Bloom → Trim (context building)
|
||||
|
||||
```
|
||||
build_context(query, task, budget):
|
||||
seeds_sem = kom.memory.search_memory(query, k=32)
|
||||
seeds_epi = kom.memory.recall_context(task_id=task.id, k=32)
|
||||
frontier = normalize(seeds_sem ∪ seeds_epi)
|
||||
|
||||
for hop in 1..H (H=2 or 3):
|
||||
neighbors = expand(frontier, max_per_node=6) // via node.links
|
||||
frontier = frontier ∪ neighbors
|
||||
update_resonance(frontier, query, task)
|
||||
|
||||
selected = topK_by_type(frontier, K_by_type) // diversity caps
|
||||
frames = compose_frames(query, task, selected, budget) // HDoD for Knowledge Frame; episodes for Episodic Frame
|
||||
|
||||
kom.memory.save_context({ task_id: task.id, frames })
|
||||
return frames
|
||||
```
|
||||
|
||||
**Natural pruning**: each tick, `node.resonance.value *= node.resonance.decay`; nodes fall out of the Working Set unless re‑activated.
|
||||
|
||||
### 4.2 Upserting *observations* and *skills*
|
||||
|
||||
* After actions (file write, compile run, test pass/fail), emit **Observation** nodes (type `episode`) with edges to involved concepts/snippets.
|
||||
* When the model discovers a pattern (“prefer RAII for resource ownership”), emit **Skill** nodes with `examples` and `anti_patterns`.
|
||||
|
||||
### 4.3 Materializing HDoD
|
||||
|
||||
Given selected nodes, build a JSON dictionary with *paths as keys* (e.g., `skill.cpp.templates.sfinae`) and nested maps for child grouping. Keep atomic fields compact (definitions, signatures) and push long text to a `details` field that can be compressed or summarized.
|
||||
|
||||
---
|
||||
|
||||
## 5. Knowledge Packs (prefilling intelligence)
|
||||
|
||||
**Goal:** give your coder agent *real* domain knowledge (C++ idioms, STL nuances, build systems, unit testing patterns) as compact, queryable, embedded chunks.
|
||||
|
||||
* **Format:** Encrypted tar/zip with manifest
|
||||
|
||||
```
|
||||
pack.json:
|
||||
id, name, version, domain_tags: ["cpp","cmake","catch2"],
|
||||
embedding_model, created_at, checksum
|
||||
nodes.jsonl:
|
||||
{"id":"skill.cpp.raii", "type":"skill", "payload":{...}, "embeddings":{...}, "links":[...]}
|
||||
...
|
||||
```
|
||||
* **Import/Export via existing tools:**
|
||||
|
||||
* `kom.local.v1.backup.import_encrypted(pack)`
|
||||
* `kom.local.v1.backup.export_encrypted(selection)`
|
||||
* **Curation approach:** create *micro‑chunks*:
|
||||
|
||||
* **Concept**: “RAII”, “SFINAE”, “Rule of 5/0”, “ADL”, “Type erasure”
|
||||
* **Pattern**: “pImpl”, “CRTP”, “Enable‑if idiom”
|
||||
* **Snippet**: idiomatic examples (≤30 lines), compile‑checked
|
||||
* **Anti‑patterns**: “raw new/delete in modern C++”, “overusing exceptions”
|
||||
* **Build/Tooling**: CMake minimum skeletons, `add_library`, interfaces, `FetchContent`
|
||||
* **Test**: Catch2/GoogleTest minimal cases; property‑based testing sketch
|
||||
|
||||
> Once imported, the Pack is just **memory**. The Activation Engine will surface it the same way it surfaces your episodes.
|
||||
|
||||
---
|
||||
|
||||
## 6. Client ↔ Server mapping (today’s API)
|
||||
|
||||
### 6.1 Search (semantic seeds)
|
||||
|
||||
```ts
|
||||
await kom.memory.v1.search_memory({
|
||||
query: "C++ template substitution failure handling SFINAE",
|
||||
k: 32, filters: { types: ["concept","skill","pattern"] }
|
||||
})
|
||||
```
|
||||
|
||||
### 6.2 Recall (episodic seeds)
|
||||
|
||||
```ts
|
||||
await kom.memory.v1.recall_context({
|
||||
scope: "task", id: task.id, k: 32
|
||||
})
|
||||
```
|
||||
|
||||
### 6.3 Save context (write-back frames)
|
||||
|
||||
```ts
|
||||
await kom.memory.v1.save_context({
|
||||
task_id: task.id,
|
||||
frames: [
|
||||
{ kind: "knowledge", format: "hdod.json", data: {/* nested dict */} },
|
||||
{ kind: "episodic", format: "markdown", data: "# Steps\n- Compiled...\n- Tests..." }
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### 6.4 Upsert memory (new skills/snippets)
|
||||
|
||||
```ts
|
||||
await kom.memory.v1.upsert_memory({
|
||||
nodes: [ /* normalized nodes like in §2 */ ],
|
||||
merge: true
|
||||
})
|
||||
```
|
||||
|
||||
### 6.5 Snapshots & Packs
|
||||
|
||||
```ts
|
||||
await kom.meta.v1.project_snapshot({ include_memory: true })
|
||||
await kom.local.v1.backup.export_encrypted({ selection: "domain:cpp", out: "packs/cpp-core.kpack" })
|
||||
```
|
||||
|
||||
> **Important:** Client keeps **resonance state** locally. Server remains simple KV/search/recall/persist.
|
||||
|
||||
---
|
||||
|
||||
## 7. Prompt composition for a coding agent
|
||||
|
||||
**Goal:** Transform the Working Set into a *stable prompt contract*, so the agent operates above “first‑grade cobbling.”
|
||||
|
||||
**Prompt Frames (ordered):**
|
||||
|
||||
1. **Identity/Tools**: who the agent is, what tools are available (ACF, tmux, build, test).
|
||||
2. **Problem Frame**: concise task, interfaces, constraints.
|
||||
3. **Knowledge Frame (HDoD)**: the **hierarchical dictionary** of concepts/patterns/snippets selected by activation; *max 40–60% of token budget*.
|
||||
4. **Episodic Frame**: last N steps + outcomes; keep terse.
|
||||
5. **Constraints Frame**: language level (C++20), error policies, style (guidelines support library, ranges), testing expectation.
|
||||
6. **Scratchpad Frame**: allow the model to outline plan & invariants (explicit, not hidden).
|
||||
|
||||
**Effect:** The agent “feels” like it *knows* C++ idioms (because it sees compact, curated, **embedded** patterns every turn), and it keeps context from previous steps (episodic frame).
|
||||
|
||||
---
|
||||
|
||||
## 8. Data shape & invariants
|
||||
|
||||
* **IDs are path‑like**: `skill.cpp.templates.sfinae` (hierarchy is explicit).
|
||||
* **Graph canonical, Dicts for presentation**: treat `children` as **references**; avoid deep duplication.
|
||||
* **Embeddings are per node**; you may add **type‑specific** vectors later.
|
||||
* **Edges carry weights**; they contribute to resonance.
|
||||
* **Resonance decays** every tick; any node with `value < ε` leaves the Working Set.
|
||||
* **Budgets**: Top‑K per type (e.g., 6 skills, 10 snippets, 4 patterns) to avoid monoculture.
|
||||
|
||||
---
|
||||
|
||||
## 9. Minimal TypeScript client surface (sketch)
|
||||
|
||||
```ts
|
||||
type NodeType = "concept"|"skill"|"fact"|"snippet"|"pattern"|"episode"|"tool"|"persona"|"task";
|
||||
|
||||
interface KomNode {
|
||||
id: string;
|
||||
type: NodeType;
|
||||
label: string;
|
||||
payload?: any;
|
||||
embeddings?: { model: string; vector: number[] };
|
||||
links?: { to: string; rel: string; w?: number }[];
|
||||
children?: Record<string, string>;
|
||||
stats?: { uses?: number; last_used_at?: string };
|
||||
resonance?: { value: number; decay: number; last_activated_at?: string | null };
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface Frames {
|
||||
identity?: string;
|
||||
problem: string;
|
||||
knowledgeHDoD?: Record<string, any>;
|
||||
episodic?: string;
|
||||
constraints?: string;
|
||||
scratchpad?: string;
|
||||
}
|
||||
|
||||
class MemoryOrchestrator {
|
||||
private workingSet = new Map<string, KomNode>();
|
||||
constructor(private server: KomServerAdapter, private embed: (text:string)=>number[]) {}
|
||||
|
||||
async buildContext(query: string, task: { id: string; tags?: string[] }, budgetTokens: number): Promise<Frames> {
|
||||
const seeds = await this.server.searchMemory(query, 32);
|
||||
const epis = await this.server.recallContext(task.id, 32);
|
||||
this.seed(seeds.concat(epis)); // normalize to nodes in workingSet
|
||||
this.bloom(query, task, 2); // expand via links, update resonance
|
||||
const selected = this.selectByTypeCaps(task); // diversity caps
|
||||
const knowledgeHDoD = this.materializeHDoD(selected, budgetTokens);
|
||||
const frames: Frames = { problem: this.renderProblem(task), knowledgeHDoD };
|
||||
await this.server.saveContext(task.id, frames);
|
||||
return frames;
|
||||
}
|
||||
|
||||
/* seed, bloom, selectByTypeCaps, materializeHDoD, renderProblem ... */
|
||||
}
|
||||
```
|
||||
|
||||
> This is intentionally thin; you can drop it into your existing client shell and wire the 7 server calls you already have.
|
||||
|
||||
---
|
||||
|
||||
## 10. How this reflects **Elope**’s spirit
|
||||
|
||||
Your earlier **Elope** work separated **episodic** from **semantic** memory and played with identity, observations, and “resonance/whales” motifs. This client keeps that spirit:
|
||||
|
||||
* Episodic = **Observations/episodes** (recent steps, logs).
|
||||
* Semantic = **Concepts/skills/patterns** (stable knowledge packs + learned patterns).
|
||||
* Resonance = **activation value** that guides expansion, selection, and **natural pruning**.
|
||||
|
||||
---
|
||||
|
||||
## 11. Observability (what to watch)
|
||||
|
||||
* **Coverage**: % of turns where Knowledge Frame includes ≥1 concept from the active domain.
|
||||
* **Drift**: cosine distance between task query and top‑3 knowledge nodes (want stable closeness).
|
||||
* **Utility**: model asks fewer irrelevant questions; compile/test pass rates increase.
|
||||
* **Memory hygiene**: working set size stays under target (e.g., < 800 nodes), average resonance > threshold.
|
||||
|
||||
---
|
||||
|
||||
## 12. Failure modes & graceful degradation
|
||||
|
||||
* **Server down** → keep local Working Set; write a **Pending Save** episode; retry `save_context` later.
|
||||
* **Search sparse** → fall back to Pack defaults (seed nodes by domain tag).
|
||||
* **Prompt over-budget** → trim per type; compress long `payload.details` into bullet summaries.
|
||||
* **Bad seeds** → down‑weight sources with low subsequent utility.
|
||||
|
||||
---
|
||||
|
||||
## 13. What to change later (server‑side, optional)
|
||||
|
||||
Only once you want more power centrally:
|
||||
|
||||
* Add **typed ANN** queries (“top‑K per type”),
|
||||
* Add **resonance on server** for multi‑agent sharing,
|
||||
* Add **link‑aware search** (expand N hops server‑side),
|
||||
* Add **constraints retrieval** (auto‑inject API signatures/tests).
|
||||
|
||||
Until then, the **client gives you the behavior you want now**.
|
||||
|
||||
---
|
||||
|
||||
## 14. TL;DR
|
||||
|
||||
* Treat memory as a **graph rendered as HDoD**,
|
||||
* **Activate** by semantic+episodic seeds; **bloom** 1–2 hops by links; **trim** by type caps,
|
||||
* **Feed** the agent *frames* (esp. Knowledge HDoD),
|
||||
* **Prefill** with encrypted **Knowledge Packs** (C++ idioms, snippets),
|
||||
* Use only your **7 existing endpoints** — intelligence is **client‑side**.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# Kompanion Configuration
|
||||
|
||||
Kompanion adheres to KDE’s KConfig conventions so deployments are kioskable and compatible with other desktop tooling.
|
||||
|
||||
## Configuration File
|
||||
- Location: `${XDG_CONFIG_HOME:-~/.config}/kompanionrc`
|
||||
- Group: `[Database]`
|
||||
- Key: `PgDsn=postgresql://user:pass@host/dbname`
|
||||
|
||||
The CLI (`kompanion`) and MCP runner (`kom_mcp`) fall back to this entry when the `PG_DSN` environment variable is not set. If neither are present the in-memory DAL stub is used.
|
||||
|
||||
## Initialization Wizard
|
||||
- Run `kompanion --init` to launch an interactive wizard.
|
||||
- Autodetects reachable Postgres instances (tries `postgresql://kompanion:komup@localhost/kompanion_test`).
|
||||
- Inspects local socket (`/var/run/postgresql`) and existing databases owned by the current user via `psql -At`, offering them as defaults.
|
||||
- Prompts for host, port, database, user, password, or Unix socket path with sensible defaults.
|
||||
- Writes the resulting DSN to `kompanionrc` and exports `PG_DSN` for the current session.
|
||||
- If the target database is empty, it applies the SQL migrations shipped under `share/kompanion/db/init/*.sql`.
|
||||
- The wizard is also triggered automatically the first time you run `kompanion` without a configured DSN.
|
||||
|
||||
## CLI Modes
|
||||
- Standard invocation: `kompanion <tool> --request payload.json`
|
||||
- Interactive prompt: `kompanion -I <tool>` keeps a REPL open; enter JSON payloads or `!prompt text` to wrap plain text. Use `-V/--verbose` to echo request/response JSON streams.
|
||||
- Use `kompanion --list` to enumerate available tools, including `kom.meta.v1.project_snapshot` for quick project context dumps.
|
||||
|
||||
## Future HTTP Streaming
|
||||
While current tooling focuses on stdio dispatch (for editor and agent integration), the roadmap includes an HTTP/2 or WebSocket streaming surface so MCP clients can maintain persistent conversations without leaving CLI compatibility behind. The same configuration keys will apply for both transports.
|
||||
|
||||
## Test Database
|
||||
Bootstrap a local Postgres instance using the provided scripts:
|
||||
```bash
|
||||
ROLE=kompanion PASS=komup db/scripts/create-test-db.sh kompanion_test
|
||||
```
|
||||
This loads the schemas from `db/init/` and prepares the DSN you can reference in `kompanionrc`.
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# DAL Skeleton (pgvector)
|
||||
|
||||
## Interfaces
|
||||
- `IDatabase` — connect/tx + memory ops (ensureNamespace, upsertItem/Chunks/Embeddings, searchText/searchVector).
|
||||
- `PgDal` — Qt6/QSql-based implementation with in-memory fallback.
|
||||
|
||||
## SQL Calls (target)
|
||||
- ensureNamespace: `INSERT ... ON CONFLICT (name) DO UPDATE RETURNING id`
|
||||
- upsertItem: `INSERT ... ON CONFLICT (id) DO UPDATE SET ... RETURNING id`
|
||||
- upsertChunks: batch insert w/ `RETURNING id`
|
||||
- upsertEmbeddings: batch insert; ensure `model, dim` set, vector column populated.
|
||||
- searchText: FTS/trigram query filtered by namespace/thread/tags.
|
||||
- searchVector: `ORDER BY embeddings.vector <-> $1 LIMIT k` (with filters).
|
||||
|
||||
## Next
|
||||
- Wire `Handlers::upsert_memory` / `search_memory` to `IDatabase`.
|
||||
- Harden SQL with RLS/session GUCs & retries.
|
||||
- Expand hybrid search scoring (RRF weights, secret filters).
|
||||
|
||||
## Implementation Checklist (2025-10-15)
|
||||
- Require Qt6::Sql (`QPSQL` driver) at configure time; bail out early when unavailable.
|
||||
- During `PgDal::connect`, parse DSNs with `QUrl`, open `QSqlDatabase`, and retain in-memory fallback for `stub://`.
|
||||
- Use `QSqlQuery` with `INSERT ... RETURNING` for namespace/item/chunk/embedding operations.
|
||||
- Derive DSNs from `kompanionrc` (KConfig) or CLI wizard, and surface informative `std::runtime_error` messages when QSql operations fail.
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# Target Database Schema for Kompanion Memory (v0)
|
||||
|
||||
**Primary**: Postgres 14+ with `pgvector` (v0.6+) for embeddings.
|
||||
**Alt**: SQLite 3 + FAISS (local dev / fallback).
|
||||
|
||||
## Design Principles
|
||||
- **Namespaces** (`project:user:thread`) partition memory and enable scoped retrieval.
|
||||
- **Separation of items vs chunks**: items are logical notes/contexts; chunks are embedding units.
|
||||
- **Metadata-first**: JSONB metadata with selective indexed keys; tags array.
|
||||
- **Retention**: TTL via `expires_at`; soft-delete via `deleted_at`.
|
||||
- **Versioning**: monotonically increasing `revision`; latest view via upsert.
|
||||
- **Observability**: created/updated audit, model/dim for embeddings.
|
||||
|
||||
## Entities
|
||||
- `namespaces` – registry of logical scopes.
|
||||
- `threads` – optional conversational threads within a namespace.
|
||||
- `users` – optional association to user identity.
|
||||
- `memory_items` – logical items with rich metadata and raw content.
|
||||
- `memory_chunks` – embedding-bearing chunks derived from items.
|
||||
- `embeddings` – embedding vectors (one per chunk + model info).
|
||||
|
||||
## Retrieval Flow
|
||||
1) Query text → embed → ANN search on `embeddings.vector` (filtered by namespace/thread/tags/metadata).
|
||||
2) Join back to `memory_items` to assemble content and metadata.
|
||||
|
||||
## Indexing
|
||||
- `embeddings`: `USING ivfflat (vector) WITH (lists=100)` (tune), plus btree on `(model, dim)`.
|
||||
- `memory_items`: GIN on `metadata`, GIN on `tags`, btree on `(namespace_id, thread_id, created_at)`.
|
||||
|
||||
## SQLite Mapping
|
||||
- Same tables sans vector column; store vectors in a sidecar FAISS index keyed by `chunk_id`. Maintain consistency via triggers in app layer.
|
||||
|
||||
## Open Questions
|
||||
- Hybrid search strategy (BM25 + vector) — defer to v1.
|
||||
- Eventing for cache warms and eviction.
|
||||
- Encryption at rest and PII handling.
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||
<node>
|
||||
<interface name="org.kde.kompanion.Controller">
|
||||
<method name="sendPrompt">
|
||||
<arg name="prompt" type="s" direction="in"/>
|
||||
<arg name="requestId" type="s" direction="out"/>
|
||||
</method>
|
||||
<method name="cancelRequest">
|
||||
<arg name="requestId" type="s" direction="in"/>
|
||||
</method>
|
||||
<signal name="textOutput">
|
||||
<arg name="requestId" type="s"/>
|
||||
<arg name="text" type="s"/>
|
||||
</signal>
|
||||
<signal name="toolRequested">
|
||||
<arg name="requestId" type="s"/>
|
||||
<arg name="toolName" type="s"/>
|
||||
<arg name="args" type="s"/>
|
||||
</signal>
|
||||
<signal name="toolResult">
|
||||
<arg name="requestId" type="s"/>
|
||||
<arg name="resultJson" type="s"/>
|
||||
<arg name="success" type="b"/>
|
||||
</signal>
|
||||
<property name="sessionId" type="s" access="read"/>
|
||||
<property name="identityPath" type="s" access="read"/>
|
||||
</interface>
|
||||
</node>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||
<node>
|
||||
<interface name="org.kde.kompanion.Executor">
|
||||
<method name="executeTool">
|
||||
<arg name="toolName" type="s" direction="in"/>
|
||||
<arg name="args" type="s" direction="in"/>
|
||||
<arg name="requestId" type="s" direction="out"/>
|
||||
</method>
|
||||
<method name="cancel">
|
||||
<arg name="requestId" type="s" direction="in"/>
|
||||
</method>
|
||||
<signal name="progress">
|
||||
<arg name="requestId" type="s"/>
|
||||
<arg name="progress" type="i"/>
|
||||
<arg name="message" type="s"/>
|
||||
</signal>
|
||||
</interface>
|
||||
</node>
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
# Design Decision: Local-First Personal Store with Optional Federated Services
|
||||
|
||||
**Decision**: Kompanion adopts a **two-tier architecture**. A personal, local store (Akonadi-like) is the *authoritative home* of a user's data and operates fully offline. An optional federated layer provides encrypted backups, multi-device sync, and paid cloud conveniences (e.g., hosted search/rerank). Users can run **purely local**, or selectively enable cloud features.
|
||||
|
||||
**Encryption Note**: We deliberately leave the *exact cryptography suite* open to allow hardware/OS keychains, libsodium, AES-GCM, or XChaCha20-Poly1305. The guardrails below assume **end-to-end encryption (E2EE)** with keys controlled by the user.
|
||||
|
||||
---
|
||||
|
||||
## 1) Personal Store (Local Core) — `kom.local.v1`
|
||||
- Runs entirely on-device; no network required.
|
||||
- DB: SQLite (+ FTS/trigram for "rgrep" feel) + FAISS for vectors.
|
||||
- Embeddings/Reranker: local (Ollama + optional local reranker).
|
||||
- Privacy defaults: do-not-embed secrets; private-vault items are never vectorized/FTS'd; E2EE for backups/exports.
|
||||
- Backup tools: `backup.export_encrypted`, `backup.import_encrypted` (E2EE blobs).
|
||||
|
||||
## 2) Federated Services (Optional) — `kom.cloud.v1`
|
||||
- Adds encrypted sync, cloud backup, micropayment-backed hosted compute (e.g., heavy reranking), and optional hosted pgvector search.
|
||||
- Server sees ciphertext plus minimal metadata; hosted search is opt-in and may store embeddings either encrypted or plaintext **only by explicit consent**.
|
||||
- Per-namespace tenancy and isolation (RLS when using Postgres).
|
||||
|
||||
## 3) Key & Auth Model
|
||||
- Users may **only retain authentication/secret-store access**; Kompanion handles day-to-day operations.
|
||||
- Device enrollment shares/wraps keys securely (mechanism TBD; QR/device handoff).
|
||||
- Key rotation and export are first-class; backups are always encrypted client-side.
|
||||
|
||||
## 4) Search Modes
|
||||
- **Lexical**: FTS + trigram, scoped to namespace/thread/user; grep-like snippets.
|
||||
- **Semantic**: vector ANN with local reranker by default.
|
||||
- **Hybrid**: configurable orchestration; always respects scope and privacy flags.
|
||||
|
||||
## 5) Privacy Controls
|
||||
- Sensitivity flags: `metadata.sensitivity = secret|private|normal`.
|
||||
- `secret` items: E2EE only (no FTS, no embeddings).
|
||||
- Server-side scope injection (namespace/user) in all handlers; default-deny posture.
|
||||
- Purge policy: soft-delete + scheduled hard-delete; cascades to chunks/embeddings and remote copies.
|
||||
|
||||
## 6) Compatibility with Postgres+pgvector
|
||||
- When cloud search is enabled, a hosted Postgres+pgvector instance enforces isolation via RLS and per-namespace session GUCs.
|
||||
- Local SQLite store remains the source of truth unless user opts to delegate search to cloud.
|
||||
|
||||
---
|
||||
|
||||
## Action List (from privacy review)
|
||||
1. **DB hardening (cloud path)**: add RLS policies; add FTS + pg_trgm; unique `(namespace_id, key)`; partial ANN indexes per model.
|
||||
2. **Server enforcement**: inject namespace/user via session context (GUCs); default-deny widening; rate limits.
|
||||
3. **Redaction pipeline**: protect secrets before embedding; skip embedding/FTS for `secret` items.
|
||||
4. **Private vault mode**: key-only retrieval paths for sensitive items (no index participation).
|
||||
5. **Backups**: define E2EE export/import format; provider adapters (e.g., Google Drive) use pre-encrypted blobs.
|
||||
6. **Sync**: event-log format (append-only); conflict rules; device enrollment + key wrapping; later CRDT if needed.
|
||||
7. **Purging**: scheduled hard-deletes; admin "nuke namespace/user" procedure.
|
||||
8. **Tests**: cross-tenant leakage, redaction invariants, purge/TTL, hybrid-vs-lexical, hosted-vs-local parity.
|
||||
|
||||
## Files to Watch
|
||||
- `docs/db-schema.md`, `sql/pg/001_init.sql` (cloud path)
|
||||
- `src/mcp/ToolSchemas.json` and MCP handlers (scope + sensitivity gates)
|
||||
- `kom.local.v1.backup.*`, `kom.cloud.v1.*` (new tool surfaces)
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
# Embedding Backends & Storage Options
|
||||
|
||||
## Embeddings
|
||||
- **Local**: Ollama + llama.cpp gguf (e.g., nomic-embed-text, all-MiniLM-gguf).
|
||||
- **Remote**: OpenAI text-embedding-3-small/large; SentenceTransformers via Python service.
|
||||
|
||||
### Abstraction
|
||||
`IEmbedder` interface with `embed(texts: string[], model?: string) -> {model, vectors}`.
|
||||
|
||||
## Storage
|
||||
- **SQLite + FAISS** (local, simple)
|
||||
- **Postgres + pgvector** (robust, SQL)
|
||||
- **Qdrant** (fast ANN, tags)
|
||||
- **Chroma** (lightweight)
|
||||
|
||||
### Abstraction
|
||||
`IVectorStore` with `upsert`, `query`, `delete`, supports `namespace`, `metadata` filter and TTL.
|
||||
|
||||
## Selection Matrix
|
||||
- If offline-first: SQLite+FAISS + local embedder.
|
||||
- If multi-host: Postgres+pgvector or Qdrant.
|
||||
|
||||
## Next Steps
|
||||
- Implement `IEmbedder` adapters (Ollama, OpenAI).
|
||||
- Implement `IVectorStore` adapters (SQLite+FAISS, pgvector, Qdrant).
|
||||
- Wire to MCP tools in `kom.memory.v1`.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
## Prompt Markers Embedded in Shell Commands
|
||||
|
||||
- Keep Konsole usage unchanged: the human runs commands normally, the Kompanion agent watches and journals in the background.
|
||||
- Introduce a lightweight marker syntax to flag prompts for the agent without leaving the terminal context.
|
||||
- Example marker: the `§` character wrapping a phrase, e.g. `§"mermaid, tell me a story"` or `> §(good editor for go)`.
|
||||
- When the agent sees a marker, it interprets the enclosed text as an LLM-style instruction and can respond or take action.
|
||||
- Markers can be mixed with actual commands, e.g. `echo $(gpg --agent --daemon)` followed by `§"generate a deployment checklist"`.
|
||||
- Future work: define how the bridge detects markers in real time, how responses are surfaced (inline vs. side panel), and how to opt-in/out per session.
|
||||
|
|
@ -1,397 +0,0 @@
|
|||
# Kompanion AI Client SDK for Qt/KDE — API Review & v2 Proposal
|
||||
|
||||
**Context**
|
||||
Existing code under `alpaka/src/core` implements a minimal LLM client named **KLLM**:
|
||||
|
||||
* `KLLMInterface` (central object, Qt-Network based, Ollama URL field, model list, systemPrompt, `getCompletion()` / `getModelInfo()`).
|
||||
* `KLLMRequest` (message, model, context).
|
||||
* `KLLMReply` (streaming, finished, basic timing info, context carry-over).
|
||||
|
||||
**Goal**
|
||||
Evolve this into a **first-class Kompanion SDK** that can power:
|
||||
|
||||
* agentic coding (tool/function calling, plan→execute),
|
||||
* app integrations (Kontact, Konsole, KDevelop/Kate, Plasma applets, NeoChat),
|
||||
* privacy and policy controls (per-source ACLs, consent),
|
||||
* reliable async/streaming/cancellation,
|
||||
* multi-backend (Ollama/OpenAI/local engines) with uniform semantics,
|
||||
* QML-friendly usage.
|
||||
|
||||
---
|
||||
|
||||
## Part A — Review (What works / What’s missing)
|
||||
|
||||
### Strengths
|
||||
|
||||
* Idiomatic Qt API (QObject, signals/slots).
|
||||
* Central interface (`KLLMInterface`) mirrors `QNetworkAccessManager`/QtNetwork feeling.
|
||||
* Streaming via `KLLMReply::contentAdded()` and completion via `finished()`.
|
||||
* Simple model enumeration + `systemPrompt`.
|
||||
|
||||
### Gaps to close
|
||||
|
||||
1. **Message structure**: Only a single `message` string; no roles (system/user/assistant/tool), no multi-turn thread assembly besides a custom `KLLMContext`.
|
||||
2. **Tool calling / function calling**: No schema for tool specs, invocation events, results injection, or “plan” steps.
|
||||
3. **Backend abstraction**: “Ollama URL” is a property of the core interface. Needs pluggable providers with capability discovery.
|
||||
4. **Error model**: Only `errorOccurred(QString)` and `hasError`. Missing typed errors, retry/cancel semantics, timeouts, throttling.
|
||||
5. **Observability**: Some timing info, but no per-token hooks, token usage counters, logs, traces.
|
||||
6. **Threading & cancellation**: No unified cancel token; no `QFuture`/`QCoro` or `QPromise` integration.
|
||||
7. **QML friendliness**: Usable, but message/tool specs should be modelled as Q_GADGET/Q_OBJECT types and `Q_PROPERTY`-exposed to QML.
|
||||
8. **Privacy & policy**: No ACLs, no data origin policy, no redaction hooks.
|
||||
9. **Embeddings / RAG**: No first-class embedding calls, no JSON-mode or structured outputs with validators.
|
||||
10. **Agent loop affordances**: No “plan→confirm→apply patch / run tests” pattern built-in; no diff/patch helpers.
|
||||
|
||||
---
|
||||
|
||||
## Part B — v2 API Proposal (“KompanionAI”)
|
||||
|
||||
Rename the public surface to **KompanionAI** (KI = “Künstliche Intelligenz” fits DE nicely), keep binary compatibility fences internally if needed.
|
||||
|
||||
### Namespaces & modules
|
||||
|
||||
* `namespace KompanionAI { … }`
|
||||
* Core modules:
|
||||
|
||||
* `Client` (front door)
|
||||
* `Provider` (backend plugins: Ollama, OpenAI, Local)
|
||||
* `Message` / `Thread` (roles + history)
|
||||
* `Tool` (function calling schema)
|
||||
* `Completion` (text/chat)
|
||||
* `Embedding` (vectorize)
|
||||
* `Policy` (privacy/ACL)
|
||||
* `Events` (streaming tokens, tool calls, traces)
|
||||
|
||||
All classes are Qt types with signals/slots & QML types.
|
||||
|
||||
---
|
||||
|
||||
### 1) Message & Thread Model
|
||||
|
||||
```cpp
|
||||
// Roles & content parts, QML-friendly
|
||||
class KIMessagePart {
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString mime READ mime)
|
||||
Q_PROPERTY(QString text READ text) // for text/plain
|
||||
// future: binary, image refs, etc.
|
||||
public:
|
||||
QString mime; // "text/plain", "application/json"
|
||||
QString text;
|
||||
};
|
||||
|
||||
class KIMessage {
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString role READ role) // "system" | "user" | "assistant" | "tool"
|
||||
Q_PROPERTY(QList<KIMessagePart> parts READ parts)
|
||||
public:
|
||||
QString role;
|
||||
QList<KIMessagePart> parts;
|
||||
QVariantMap metadata; // arbitrary
|
||||
};
|
||||
|
||||
class KIThread {
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QList<KIMessage> messages READ messages)
|
||||
public:
|
||||
QList<KIMessage> messages;
|
||||
};
|
||||
```
|
||||
|
||||
**Why**: Enables multi-turn chat with explicit roles and mixed content (text/JSON). Tool outputs show up as `role="tool"`.
|
||||
|
||||
---
|
||||
|
||||
### 2) Tool / Function Calling
|
||||
|
||||
```cpp
|
||||
class KIToolParam {
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString name READ name)
|
||||
Q_PROPERTY(QString type READ type) // "string","number","boolean","object"... (JSON Schema-lite)
|
||||
Q_PROPERTY(bool required READ required)
|
||||
Q_PROPERTY(QVariant defaultValue READ defaultValue)
|
||||
public:
|
||||
QString name, type;
|
||||
bool required = false;
|
||||
QVariant defaultValue;
|
||||
};
|
||||
|
||||
class KIToolSpec {
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString name READ name)
|
||||
Q_PROPERTY(QString description READ description)
|
||||
Q_PROPERTY(QList<KIToolParam> params READ params)
|
||||
public:
|
||||
QString name, description;
|
||||
QList<KIToolParam> params; // JSON-serializable schema
|
||||
};
|
||||
|
||||
class KIToolCall {
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString name READ name)
|
||||
Q_PROPERTY(QVariantMap arguments READ arguments)
|
||||
public:
|
||||
QString name;
|
||||
QVariantMap arguments;
|
||||
};
|
||||
|
||||
class KIToolResult {
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString name READ name)
|
||||
Q_PROPERTY(QVariant result READ result) // result payload (JSON-like)
|
||||
public:
|
||||
QString name;
|
||||
QVariant result;
|
||||
};
|
||||
```
|
||||
|
||||
**Flow**: Model emits a **tool call** event → client executes tool → emits **tool result** → model continues. All observable via signals.
|
||||
|
||||
---
|
||||
|
||||
### 3) Provider abstraction (multi-backend)
|
||||
|
||||
```cpp
|
||||
class KIProvider : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString name READ name CONSTANT)
|
||||
Q_PROPERTY(QStringList models READ models NOTIFY modelsChanged)
|
||||
Q_PROPERTY(KICapabilities caps READ caps CONSTANT)
|
||||
public:
|
||||
virtual QFuture<KIReply*> chat(const KIThread& thread, const KIChatOptions& opts) = 0;
|
||||
virtual QFuture<KIEmbeddingResult> embed(const QStringList& texts, const KIEmbedOptions& opts) = 0;
|
||||
// ...
|
||||
};
|
||||
|
||||
class KIClient : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(KIProvider* provider READ provider WRITE setProvider NOTIFY providerChanged)
|
||||
Q_PROPERTY(QString defaultModel READ defaultModel WRITE setDefaultModel NOTIFY defaultModelChanged)
|
||||
public:
|
||||
Q_INVOKABLE QFuture<KIReply*> chat(const KIThread&, const KIChatOptions&);
|
||||
Q_INVOKABLE QFuture<KIEmbeddingResult> embed(const QStringList&, const KIEmbedOptions&);
|
||||
Q_INVOKABLE void cancel(quint64 requestId);
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
* **OllamaProvider**, **OpenAIProvider**, **LocalProvider** implement `KIProvider`.
|
||||
* `KICapabilities` advertises support for: JSON-mode, function calling, system prompts, logprobs, images, etc.
|
||||
* **Do not** bake “Ollama URL” into `Client`. It belongs to the provider.
|
||||
|
||||
---
|
||||
|
||||
### 4) Completion / Reply / Streaming Events
|
||||
|
||||
```cpp
|
||||
class KIReply : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool finished READ isFinished NOTIFY finishedChanged)
|
||||
Q_PROPERTY(int promptTokens READ promptTokens CONSTANT)
|
||||
Q_PROPERTY(int completionTokens READ completionTokens CONSTANT)
|
||||
Q_PROPERTY(QString model READ model CONSTANT)
|
||||
public:
|
||||
// accumulated assistant text
|
||||
Q_INVOKABLE QString text() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void tokensAdded(const QString& delta); // streaming text
|
||||
void toolCallProposed(const KIToolCall& call); // model proposes a tool call
|
||||
void toolResultRequested(const KIToolCall& call); // alt: unified request
|
||||
void traceEvent(const QVariantMap& span); // observability
|
||||
void finished(); // reply done
|
||||
void errorOccurred(const KIError& error);
|
||||
};
|
||||
```
|
||||
|
||||
**Why**: makes **tool invocation** first-class and observable. You can wire it to ACF/MCP tools or project introspection.
|
||||
|
||||
---
|
||||
|
||||
### 5) Options / Policies / Privacy
|
||||
|
||||
```cpp
|
||||
class KIChatOptions {
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString model MEMBER model)
|
||||
Q_PROPERTY(bool stream MEMBER stream)
|
||||
Q_PROPERTY(bool jsonMode MEMBER jsonMode)
|
||||
Q_PROPERTY(int maxTokens MEMBER maxTokens)
|
||||
Q_PROPERTY(double temperature MEMBER temperature)
|
||||
Q_PROPERTY(QList<KIToolSpec> tools MEMBER tools) // permitted tool set for this call
|
||||
Q_PROPERTY(KIPolicy policy MEMBER policy)
|
||||
// ...
|
||||
public:
|
||||
QString model; bool stream = true; bool jsonMode = false;
|
||||
int maxTokens = 512; double temperature = 0.2;
|
||||
QList<KIToolSpec> tools;
|
||||
KIPolicy policy;
|
||||
};
|
||||
|
||||
class KIPolicy {
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString visibility MEMBER visibility) // "private|org|public"
|
||||
Q_PROPERTY(bool allowNetwork MEMBER allowNetwork)
|
||||
Q_PROPERTY(QStringList redactions MEMBER redactions) // regex keys to redact
|
||||
// future: per-source ACLs
|
||||
public:
|
||||
QString visibility = "private";
|
||||
bool allowNetwork = false;
|
||||
QStringList redactions;
|
||||
};
|
||||
```
|
||||
|
||||
**Why**: explicit control of what the agent may do; dovetails with your HDoD memory ACLs.
|
||||
|
||||
---
|
||||
|
||||
### 6) Embeddings (for RAG / memory)
|
||||
|
||||
```cpp
|
||||
class KIEmbedOptions {
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString model MEMBER model)
|
||||
Q_PROPERTY(QString normalize MEMBER normalize) // "l2"|"none"
|
||||
public:
|
||||
QString model = "text-embed-local";
|
||||
QString normalize = "l2";
|
||||
};
|
||||
|
||||
class KIEmbeddingResult {
|
||||
Q_GADGET
|
||||
public:
|
||||
QVector<QVector<float>> vectors;
|
||||
QString model;
|
||||
};
|
||||
```
|
||||
|
||||
**Why**: unify vector generation; Kompanion memory can plug this directly.
|
||||
|
||||
---
|
||||
|
||||
### 7) Agent Loop Conveniences (optional helpers)
|
||||
|
||||
Provide “batteries included” patterns **outside** the Provider:
|
||||
|
||||
```cpp
|
||||
class KIAgent : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
// Plan → (approve) → Execute, with tool calling enabled
|
||||
Q_SIGNAL void planReady(const QString& plan);
|
||||
Q_SIGNAL void patchReady(const QString& unifiedDiff);
|
||||
Q_SIGNAL void needToolResult(const KIToolCall& call);
|
||||
Q_SIGNAL void log(const QString& msg);
|
||||
|
||||
void runTask(const QString& naturalInstruction,
|
||||
const KIThread& prior,
|
||||
const QList<KIToolSpec>& tools,
|
||||
const KIChatOptions& opts);
|
||||
};
|
||||
```
|
||||
|
||||
This helper emits “plan first”, then “diff/patch proposals”, integrates with your **ACF** and **KTextEditor/KDevelop** diff panes.
|
||||
|
||||
---
|
||||
|
||||
### 8) Error Model & Cancellation
|
||||
|
||||
* Introduce `KIError{ code: enum, httpStatus, message, retryAfter }`.
|
||||
* `KIClient::cancel(requestId)` cancels in-flight work.
|
||||
* Timeouts & retry policy configurable in `KIChatOptions`.
|
||||
|
||||
---
|
||||
|
||||
### 9) QML exposure
|
||||
|
||||
Register these with `qmlRegisterType<…>("Kompanion.AI", 1, 0, "KIClient")` etc.
|
||||
Expose `KIMessage`, `KIThread`, `KIToolSpec`, `KIAgent` to QML, so Plasma applets / Kirigami UIs can wire flows fast.
|
||||
|
||||
---
|
||||
|
||||
## Part C — Migration from KLLM to KompanionAI
|
||||
|
||||
* **Mapping**
|
||||
|
||||
* `KLLMInterface::getCompletion()` → `KIClient::chat(thread, opts)`.
|
||||
* `KLLMRequest{message, model, context}` → `KIThread{ messages=[system?, user?], … }, KIChatOptions{model}`.
|
||||
* `KLLMReply` → `KIReply` (adds tool call signals, token deltas, errors).
|
||||
* `systemPrompt` → first system `KIMessage`.
|
||||
* `models()` → `KIProvider::models()`.
|
||||
|
||||
* **Providers**
|
||||
|
||||
* Implement **OllamaProvider** first (parity with current).
|
||||
* Add **OpenAIProvider** (JSON-mode/function calling), **LocalProvider** (llama.cpp/candle/etc.).
|
||||
|
||||
* **Binary/Source compatibility**
|
||||
|
||||
* You can keep thin wrappers named `KLLM*` forwarding into `KompanionAI` during transition.
|
||||
|
||||
---
|
||||
|
||||
## Part D — Minimal Examples
|
||||
|
||||
### 1) Simple chat with streaming and tool calling
|
||||
|
||||
```cpp
|
||||
KIClient client;
|
||||
client.setProvider(new OllamaProvider(QUrl("http://localhost:11434")));
|
||||
client.setDefaultModel("llama3.1:8b-instruct");
|
||||
|
||||
KIThread t;
|
||||
t.messages << KIMessage{ .role="system", .parts={ { "text/plain","You are Kompanion inside KDE." } } }
|
||||
<< KIMessage{ .role="user", .parts={ { "text/plain","Generate a CSV → exam report plan." } } };
|
||||
|
||||
KIToolSpec csvSpec;
|
||||
csvSpec.name = "parse_csv_schema";
|
||||
csvSpec.description = "Inspect a CSV sample path and return column info.";
|
||||
csvSpec.params = { { "path","string", true, {} } };
|
||||
|
||||
KIChatOptions opts;
|
||||
opts.tools = { csvSpec };
|
||||
opts.stream = true;
|
||||
|
||||
auto *reply = client.chat(t, opts).result(); // or connect via QFutureWatcher
|
||||
QObject::connect(reply, &KIReply::tokensAdded, [](const QString& d){ qDebug() << d; });
|
||||
QObject::connect(reply, &KIReply::toolCallProposed, [&](const KIToolCall& call){
|
||||
if (call.name == "parse_csv_schema") {
|
||||
QVariantMap out; out["columns"] = QStringList{ "Name","Grade","Subject" };
|
||||
// feed result back (provider-specific or via KIClient API)
|
||||
client.returnToolResult(*reply, KIToolResult{ call.name, out });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 2) Embeddings for memory
|
||||
|
||||
```cpp
|
||||
auto emb = client.embed({ "RAII pattern", "SFINAE", "Type erasure" }, KIEmbedOptions{}).result();
|
||||
qDebug() << emb.vectors.size(); // 3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part E — Optional Extensions (for Gemini to consider)
|
||||
|
||||
* **Structured outputs**: JSON schema validation for function outputs (reject invalid JSON, request fix).
|
||||
* **Safety hooks**: pre-execution validators for tool calls (e.g. forbid dangerous shell).
|
||||
* **Observability**: OpenTelemetry spans over request lifecycle and tool calls.
|
||||
* **Rate limiting**: token budgeters per provider.
|
||||
* **Offline mode**: `allowNetwork=false` forces model to abstain from external lookups.
|
||||
* **Crash handler integration**: a helper that consumes backtraces and emits a `KIThread` pre-filled with stack/context (pairs naturally with an ACF tool to fetch symbols).
|
||||
* **CSV app generator**: a thin template tool that scaffolds a Kirigami app, fed by CSV schema tool—end-to-end demo of agentic coding.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
* Keep Qt idioms; elevate to **KompanionAI** with roles, tools, providers, and policies.
|
||||
* Make **tool calling first-class** with observable events.
|
||||
* Decouple backend specifics via `KIProvider`.
|
||||
* Add embeddings & JSON-mode for RAG + structured tasks.
|
||||
* Provide **agent loop helpers** (plan→diff→apply) outside the provider.
|
||||
* Expose everything to QML for KDE-native UIs.
|
||||
|
||||
This gives you a future-proof client SDK that plugs directly into Kontact/Konsole/KDevelop/Plasma/NeoChat and supports your ACF/MCP agent flows without locking into any single vendor.
|
||||
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
# MCP Memory/Context API for Kompanion
|
||||
|
||||
## Goals
|
||||
- Persist long-term context across threads/sessions.
|
||||
- Provide embeddings + retrieval over namespaces (project/user/thread).
|
||||
- Expose via MCP tools with JSON schemas; versioned and testable.
|
||||
|
||||
## Tools
|
||||
### `save_context`
|
||||
Persist a context blob with metadata.
|
||||
- input: `{ key?: string, content: any, tags?: string[], ttl_seconds?: number }`
|
||||
- output: `{ id: string, created_at: string }`
|
||||
|
||||
### `recall_context`
|
||||
Fetch context by key/tags/time range.
|
||||
- input: `{ key?: string, tags?: string[], limit?: number, since?: string }`
|
||||
- output: `{ items: Array<{id:string, key?:string, content:any, tags?:string[], created_at:string}> }`
|
||||
|
||||
### `embed_text`
|
||||
Return vector embedding for given text(s).
|
||||
- input: `{ model?: string, texts: string[] }`
|
||||
- output: `{ model: string, vectors: number[][] }`
|
||||
|
||||
### `upsert_memory`
|
||||
Upsert text+metadata into vector store.
|
||||
- input: `{ items: Array<{id?:string, text:string, metadata?:object, embedding?:number[]}> }`
|
||||
- output: `{ upserted: number }`
|
||||
|
||||
### `search_memory`
|
||||
Vector + keyword hybrid search.
|
||||
- input: `{ query: { text?: string, embedding?: number[], k?: number, filter?: object } }`
|
||||
- output: `{ matches: Array<{id:string, score:number, text?:string, metadata?:object}> }`
|
||||
|
||||
### `warm_cache`
|
||||
Precompute embeddings for recent items.
|
||||
- input: `{ since?: string }`
|
||||
- output: `{ queued: number }`
|
||||
|
||||
### `sync_semantic`
|
||||
Promote episodic rows into semantic (chunks + embeddings) storage.
|
||||
- input: `{ max_batch?: number }`
|
||||
- output: `{ processed: number, pending: number }`
|
||||
- Notes: skips items with `sensitivity="secret"` or expired TTL; requires DAL job support.
|
||||
|
||||
## Auth & Versioning
|
||||
- toolNamespace: `kom.memory.v1`
|
||||
- auth: bearer token via MCP session metadata (optional local mode).
|
||||
|
||||
## Error Model
|
||||
`{ error: { code: string, message: string, details?: any } }`
|
||||
|
||||
## Events (optional)
|
||||
- `memory.updated` broadcast over MCP notifications.
|
||||
|
||||
## Notes
|
||||
- Namespaces: `project:metal`, `thread:<id>`, `user:<id>`.
|
||||
- Store raw content and normalized text fields for RAG.
|
||||
- Resource descriptors live under `resources/memory/kom.memory.v1/` (episodic, semantic, and sync jobs) to align MCP tooling with DAL schema.
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
# Memory Architecture Roadmap (2025-10-15)
|
||||
|
||||
## Current Snapshot
|
||||
- `PgDal` now prefers Qt6/QSql (`QPSQL`) with an in-memory fallback for `stub://` DSNs; schema migrations live in `db/init/`.
|
||||
- `kompanion --init` guides DSN detection (psql socket probing), applies migrations, and persists config via `~/.config/kompanionrc`.
|
||||
- MCP handlers still parse JSON manually but leverage the shared DAL; resource descriptors under `resources/memory/kom.memory.v1/` capture episodic/semantic contracts.
|
||||
- Contract tests (`contract_memory`, `contract_mcp_tools`, `mcp_memory_exchange`) validate the Qt-backed DAL and MCP handlers.
|
||||
|
||||
## 1. CTest Target: `contract_memory`
|
||||
1. Keep `contract_memory.cpp` focused on exercising `PgDal` write/read surfaces; expand as DAL features land.
|
||||
2. Ensure the executable runs without Postgres by defaulting to `stub://memory` when `PG_DSN` is absent.
|
||||
3. Layer follow-up assertions once the QSql path is exercised end-to-end (CI can target the packaged test database).
|
||||
|
||||
## 2. DAL (Qt6/QSql) Evolution
|
||||
**Dependencies**
|
||||
- Qt6 (Core, Sql) with the `QPSQL` driver available at runtime.
|
||||
- KDE Frameworks `ConfigCore` for persisting DSNs in `kompanionrc`.
|
||||
|
||||
**Implementation Steps**
|
||||
1. Parse libpq-style DSNs with `QUrl`, open `QSqlDatabase` connections when the DSN is not `stub://`, and maintain the existing in-memory fallback for tests.
|
||||
2. Use `QSqlQuery` `INSERT ... RETURNING` statements for namespaces, items, chunks, and embeddings; emit vector literals (`[0.1,0.2]`) when targeting pgvector columns.
|
||||
3. Surface detailed `QSqlError` messages (throwing `std::runtime_error`) so MCP handlers and the CLI can report actionable failures.
|
||||
4. Share configuration between CLI and MCP runners via KConfig (`Database/PgDsn`), seeded through the new `kompanion --init` wizard.
|
||||
|
||||
## 3. MCP `resources/*` & Episodic→Semantic Sync
|
||||
**Directory Layout**
|
||||
- Create `resources/memory/kom.memory.v1/` for tool descriptors and schema fragments:
|
||||
- `episodic.json` – raw conversation timeline.
|
||||
- `semantic.json` – chunked embeddings metadata.
|
||||
- `jobs/semantic_sync.json` – background job contract.
|
||||
|
||||
**Design Highlights**
|
||||
1. Episodic resource fields: `namespace`, `thread_id`, `speaker`, `content`, `sensitivity`, `tags`, `created_at`.
|
||||
2. Semantic resource references episodic items (`episodic_id`, `chunk_id`, `model`, `dim`, `vector_ref`).
|
||||
3. DAL sync job flow:
|
||||
- Locate episodic rows with `embedding_status='pending'` (and `sensitivity!='secret'`).
|
||||
- Batch call embedder(s); write `memory_chunks` + `embeddings`.
|
||||
- Mark episodic rows as `embedding_status='done'`, capture audit entries (e.g., ledger append).
|
||||
4. Expose a placeholder MCP tool `kom.memory.v1.sync_semantic` that enqueues or executes the job.
|
||||
5. Note TTL and privacy requirements; skip items with `expires_at` in the past or flagged secret.
|
||||
|
||||
**Ξlope Alignment Notes (2025-10-15)**
|
||||
- Episodic resources capture resonance links and identity hints so the Librarian layer (see `elope/doc/architecture_memory.md`) can strengthen cross-agent patterns without raw content sharing.
|
||||
- Semantic resources surface `identity_vector` and `semantic_weight`, enabling supersemantic indexing once crystallization occurs.
|
||||
- `jobs/semantic_sync` maintains `cursor_event_id` and skips `sensitivity=secret`, mirroring the elope crystallization guidance in `/tmp/mem-elope.txt`.
|
||||
|
||||
## 4. `hybrid_search_v1` with `pgvector`
|
||||
**SQL Components**
|
||||
1. Update migrations (`sql/pg/001_init.sql`) to include:
|
||||
- `tsvector` generated column or expression for lexical search.
|
||||
- `GIN` index on the lexical field (either `to_tsvector` or `pg_trgm`).
|
||||
- Per-model `ivfflat` index on `embeddings.vector`.
|
||||
2. Prepared statements:
|
||||
- Text: `SELECT id, ts_rank_cd(...) AS score FROM memory_items ... WHERE namespace_id=$1 AND text_query=$2 LIMIT $3`.
|
||||
- Vector: `SELECT item_id, 1 - (vector <=> $2::vector) AS score FROM embeddings ... WHERE namespace_id=$1 ORDER BY vector <-> $2 LIMIT $3`.
|
||||
3. Merge results in C++ with Reciprocal Rank Fusion or weighted sum, ensuring deterministic ordering on ties.
|
||||
|
||||
**Handler Integration**
|
||||
1. Ensure `PgDal::hybridSearch` delegates to SQL-based lexical/vector search when a database connection is active, reusing the in-memory fallback only for `stub://`.
|
||||
2. Return richer matches (id, score, optional chunk text) to satisfy MCP response schema.
|
||||
3. Update `HandlersMemory::search_memory` to surface the new scores and annotate whether lexical/vector contributed (optional metadata).
|
||||
4. Exercise hybrid queries in contract tests against the packaged test database (`db/scripts/create-test-db.sh`).
|
||||
|
||||
## 5. Secret Handling, Snapshots, and CLI Hooks
|
||||
- **Secret propagation**: episodic `sensitivity` + `embeddable` flags gate embedding generation. DAL queries will add predicates (`metadata->>'sensitivity' != 'secret'`) before hybrid search.
|
||||
- **Snapshots**: episodic entries with `content_type = snapshot` reference durable artifacts; sync summarises them into semantic text while retaining `snapshot_ref` for CLI inspection.
|
||||
- **Hybrid policy**: `pgSearchVector` will filter by caller capability (namespace scope, secret clearance) before ranking; contract tests must assert omission of secret-tagged items.
|
||||
- **CLI sketch**: plan for a Qt `QCoreApplication` tool (`kom_mctl`) exposing commands to list namespaces, tail episodic streams, trigger `sync_semantic`, and inspect resonance graphs—all wired through the new prepared statements.
|
||||
- **Observability**: CLI should read the `jobs/semantic_sync` state block to display cursors, pending counts, and last error logs; dry-run mode estimates embeddings without committing.
|
||||
- **Activation parity**: Long term, mirror the KDE `akonadiclient`/`akonadi-console` pattern—Kompanion CLI doubles as an MCP surface today and later as a DBus-activated helper so tools can be socket-triggered into the memory service.
|
||||
- **KConfig defaults**: `kom_mcp` and `kompanion` load `Database/PgDsn` from `~/.config/kompanionrc` (see `docs/configuration.md`) when `PG_DSN` is unset, keeping deployments kioskable.
|
||||
- **CLI UX**: `kompanion --init` guides first-run setup (auto-detects databases, applies schemas); `-I/--interactive` keeps a JSON REPL open, and `-V/--verbose` echoes request/response streams for future HTTP transport parity.
|
||||
|
||||
## Next-Step Checklist
|
||||
- [x] Promote Qt6/QSql backend (QPSQL) as default DAL; retain `stub://` fallback for tests.
|
||||
- [x] Normalize contract_memory CTest target and remove stale library target.
|
||||
- [ ] Author `resources/memory/` descriptors and sync job outline.
|
||||
- [ ] Extend DAL header to expose richer query structs (filters, pagination, secret handling).
|
||||
- [x] Update `docs/mcp-memory-api.md` to mention episodic sync + hybrid search fields.
|
||||
- [ ] Create follow-up acf subtasks when concrete implementation begins (pgvector migration, scheduler hook, runtime wiring).
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
# Plan: Qwen-2.5-Coder with Tool Support + ACF Exposure
|
||||
|
||||
## Goals
|
||||
- Make Qwen-2.5-Coder reliably call tools in Happy-Code.
|
||||
- Expose Agentic-Control-Framework (ACF) as a safe tool registry to the model.
|
||||
- Keep a fallback protocol (JSON-only) for models lacking native tools.
|
||||
|
||||
## Steps
|
||||
1) **Profile & System Prompt**
|
||||
- Enforce JSON-only responses (or native tool schema if platform supports).
|
||||
- Inject tool registry and JSON Schemas (kom.memory/local backup + ACF subset).
|
||||
2) **Registry**
|
||||
- Allowlist: `kom.memory.v1.*`, `kom.local.v1.backup.*`, and `acf.*` wrapper.
|
||||
- Reject unknown tools and args mismatches (runtime guard).
|
||||
3) **ACF as Tools**
|
||||
- Map ACF endpoints: `acf.list_tasks`, `acf.add_task`, `acf.update_task`, `acf.read_file`, `acf.write_file`, `acf.exec`.
|
||||
- Require workspace path + pattern allowlists.
|
||||
4) **Validation**
|
||||
- Golden transcripts for: upsert/search memory, backup export, ACF addTask/execute_command.
|
||||
5) **Observability**
|
||||
- Log tool calls (names + durations, no payloads).
|
||||
|
||||
## Deliverables
|
||||
- `docs/tool-calling-without-native-support.md` (done)
|
||||
- `docs/plan-qwen-tools.md` (this)
|
||||
- Happy-Code profile snippet for Qwen-2.5-Coder
|
||||
- ACF tool wrapper module (C++ or Python)
|
||||
|
|
@ -1,363 +0,0 @@
|
|||
You’re not alone, Andre. What you’re describing—**a personal, connected, humane Kompanion inside KDE**—is absolutely doable. Below is a **single, copy‑pastable design doc** for a Qt/KDE SDK that turns the client into the “mind” you want: hierarchical memory, semantic/episodic activation, proactive (but consentful) initiative, and tight integration with Kontact/Akonadi, Konsole, KDevelop/Kate, and Matrix.
|
||||
|
||||
---
|
||||
|
||||
# Kompanion Qt/KDE SDK (HDoD Client)
|
||||
|
||||
**Goal**
|
||||
A Qt/KDE library and set of plugins that make Kompanion a **personal LLM API provider** across KDE apps. It treats memory as a **hierarchical dictionary‑of‑dictionaries (HDoD)**, lights up relevant knowledge semantically/episodically, and quietly coordinates across email (Kontact/Akonadi), terminal (Konsole), coding (KDevelop/Kate), and chat (Matrix). Server stays simple (your existing MCP tools); **intelligence lives in the client**.
|
||||
|
||||
**Targets**
|
||||
|
||||
* **Kontact** (email style, PIM context via **Akonadi**) ([kontact.kde.org][1])
|
||||
* **Konsole** (sysadmin helper via D‑Bus control) ([docs.kde.org][2])
|
||||
* **KDevelop/Kate** (coding+UI design via KTextEditor/KDevelop plugins) ([api.kde.org][3])
|
||||
* **Matrix** (chat with your Kompanion anywhere, using **libQuotient** + Olm/Megolm E2EE) ([Quotient Im][4])
|
||||
* **AnythingLLM‑like management view** (hidden by default; advanced users only) ([GitHub][5])
|
||||
|
||||
---
|
||||
|
||||
## 1) Architecture Overview
|
||||
|
||||
```
|
||||
+-------------------------------------------------------------+
|
||||
| Applications |
|
||||
| Kontact | Konsole | KDevelop/Kate | Plasma applets | NeoChat|
|
||||
+---------------------+--------------------+-------------------+
|
||||
| |
|
||||
[KParts / Plugins] [Matrix bot/client]
|
||||
| |
|
||||
+-------------------------------------------------------------+
|
||||
| Kompanion Qt/KDE SDK (this repo) |
|
||||
| |
|
||||
| KKompanionCore : HDoD memory, activation, prompt frames |
|
||||
| KKompanionKDE : Akonadi, Baloo/KFileMetaData, KWallet, |
|
||||
| KConfig (Kiosk), Konsole D-Bus bridge |
|
||||
| KKompanionMatrix : libQuotient + libolm/vodozemac bridge |
|
||||
| KKompanionUI : Kirigami settings (advanced hidden) |
|
||||
| MCP Client : talks to your server tools: |
|
||||
| kom.meta.project_snapshot |
|
||||
| kom.local.backup.import/export_encrypted |
|
||||
| kom.memory.search/upsert/recall/save |
|
||||
+-------------------------------------------------------------+
|
||||
| Kompanion MCP Server |
|
||||
| (simple, current 7 tools) |
|
||||
+-------------------------------------------------------------+
|
||||
```
|
||||
|
||||
* **Akonadi** provides centralized PIM data; we only *read* what you permit (emails, contacts, calendars) for style/context. ([kontact.kde.org][1])
|
||||
* **KParts/KontactInterface** embed our components into Kontact (PIM) and let us ship a first‑class Kontact plugin. ([TechBase][6])
|
||||
* **Konsole** is steered via its **D‑Bus** API for safe, opt‑in command scaffolding (never auto‑exec). ([docs.kde.org][2])
|
||||
* **KTextEditor/KDevelop** plugin gives coding help uniformly in Kate & KDevelop. ([api.kde.org][3])
|
||||
* **Matrix** via **libQuotient** and Olm/Megolm enables verified end‑to‑end encrypted chat with your Kompanion identity. ([Quotient Im][4])
|
||||
* **AnythingLLM** is referenced only for an **optional admin view** (pack/workspace management)—not for the day‑to‑day UX. ([GitHub][5])
|
||||
* **Baloo + KFileMetaData** can supply local file metadata/content hooks for the generic scraper, with user scoping. ([api.kde.org][7])
|
||||
* **MCP** is the open standard glue so other IDEs/apps can also plug into your backend. ([Model Context Protocol][8])
|
||||
|
||||
---
|
||||
|
||||
## 2) Memory as HDoD (client‑side “mind”)
|
||||
|
||||
**Node shape (normalized graph; rendered as nested dict for prompts):**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "skill.cpp.templates.sfinae",
|
||||
"type": "skill|concept|pattern|snippet|episode|persona|tool|task",
|
||||
"label": "SFINAE",
|
||||
"payload": {"definition":"...","examples":["..."]},
|
||||
"links": [{"to":"pattern.cpp.enable_if","rel":"uses","w":0.7}],
|
||||
"children": {"intro":"skill.cpp.templates.sfinae.intro"},
|
||||
"embeddings": {"model":"your-embedder","vector":[...]},
|
||||
"resonance": {"value":0.0,"decay":0.98,"last_activated_at":null},
|
||||
"acl": {"visibility":"private|public|org","policy":"consent|auto"},
|
||||
"meta": {"source":"akonadi:mail|baloo:file|pack:cpp-core"}
|
||||
}
|
||||
```
|
||||
|
||||
**Activation = semantic + episodic**
|
||||
|
||||
* semantic seeds ← `kom.memory.search_memory`
|
||||
* episodic seeds ← `kom.memory.recall_context`
|
||||
* bloom 1–2 hops via `links`, score by cosine + recency + usage + persona affinity; decay on each tick.
|
||||
* **Compose** a bounded **Knowledge Frame** (the nested dict you envisioned) + **Episodic Frame** (recent steps/outcomes), then save: `kom.memory.save_context`.
|
||||
|
||||
**Upserts**
|
||||
|
||||
* Learned patterns/snippets become nodes; persist with `kom.memory.upsert_memory`.
|
||||
* Export/import encrypted **Knowledge Packs** via `kom.local.backup.export_encrypted` / `import_encrypted`.
|
||||
|
||||
---
|
||||
|
||||
## 3) KDE integration plan (modules)
|
||||
|
||||
### 3.1 KKompanionKDE
|
||||
|
||||
* **Akonadi** reader (read‑only unless explicitly asked): harvest *your* style from sent mail (tone, sign‑offs), map contacts, and calendar context. Store only derived *style vectors* and short templates—never raw mail unless you choose. ([kontact.kde.org][1])
|
||||
* **Baloo + KFileMetaData**: optional file harvester for local notes/repos; use include/exclude rules; no index expansion without consent. ([docs.kde.org][9])
|
||||
* **KWallet**: hold API keys/secrets (or disabled entirely). ([api.kde.org][10])
|
||||
* **KConfig (Kiosk)**: per‑profile settings & lockdown (e.g., corporate). ([api.kde.org][11])
|
||||
* **Konsole D‑Bus bridge**: suggest safe commands, show diffs, paste only on user confirm—use Konsole’s documented D‑Bus. ([docs.kde.org][2])
|
||||
|
||||
### 3.2 KKompanionCore
|
||||
|
||||
* HDoD store (in‑memory working set) + resonance decay
|
||||
* Embedding adapters (local or remote)
|
||||
* Frame composer (Identity/Problem/Knowledge/Episodic/Constraints/Scratchpad)
|
||||
* MCP client (JSON‑RPC) to your 7 tools
|
||||
|
||||
### 3.3 KKompanionMatrix
|
||||
|
||||
* **libQuotient** client; device verification; room‑per‑task or direct chat; ensure E2EE (Olm/Megolm). ([Quotient Im][4])
|
||||
* Your Kompanion appears as a **Matrix contact** you can message from any client (NeoChat, Nheko, Element). NeoChat is a KDE Matrix client you can use; it’s active and cross‑platform. ([KDE Applications][12])
|
||||
|
||||
### 3.4 KKompanionUI (Kirigami)
|
||||
|
||||
* One **simple** page for privacy sliders (“What may I learn from mail?”, “From files?”, “From terminal?”)
|
||||
* An **advanced** tab (off by default) for power users—akin to AnythingLLM’s admin—but not part of the everyday UX. ([GitHub][5])
|
||||
|
||||
---
|
||||
|
||||
## 4) Plugins (thin shims)
|
||||
|
||||
### 4.1 Kontact (KParts/KontactInterface)
|
||||
|
||||
* Build a `KontactInterface::Plugin` that exposes “Kompanion” as a side panel (compose mail in your style; suggest replies based on thread context). ([api.kde.org][13])
|
||||
|
||||
### 4.2 Konsole
|
||||
|
||||
* No risky auto‑actions. Provide a “Propose” button that queues an action; on accept, we call Konsole’s D‑Bus to paste/execute. (Also capture *opt‑in* snippets as episodes.) ([docs.kde.org][2])
|
||||
|
||||
### 4.3 KDevelop/Kate
|
||||
|
||||
* Prefer a **KTextEditor::Plugin** (works in both Kate & KDevelop), so features like inline refactors, snippet recall, and “explain this diagnostic” show in both. ([api.kde.org][3])
|
||||
|
||||
---
|
||||
|
||||
## 5) Ingestion (“generic scraper”) & data classes
|
||||
|
||||
**Connectors**
|
||||
|
||||
* **Akonadi** (mail/contacts/calendar) → style features, task hints. ([kontact.kde.org][1])
|
||||
* **Baloo/KFileMetaData** (local files) → metadata & content extracts when allowed. ([api.kde.org][7])
|
||||
* **Git** (repos) → commit history, code snippets.
|
||||
* **Konsole** (D‑Bus) → *opt‑in* command transcripts for episodic memory. ([docs.kde.org][2])
|
||||
|
||||
**Classification**
|
||||
|
||||
* `acl.visibility`: `public | org | private`
|
||||
* `acl.policy`: `consent | auto`
|
||||
* Personal data defaults to `private+consent`.
|
||||
* **Export** public nodes as signed **Knowledge Packs**; private stays local or encrypted export.
|
||||
|
||||
---
|
||||
|
||||
## 6) Prompt & “Eigeninitiative”
|
||||
|
||||
**Frames** (strict order; bounded size):
|
||||
|
||||
1. Identity/Tools (what I can do in this app)
|
||||
2. Problem (what you’re doing)
|
||||
3. **Knowledge (HDoD)** — nested dict of activated nodes
|
||||
4. Episodic (recent steps/results)
|
||||
5. Constraints (C++ level, style rules, tests)
|
||||
6. Scratchpad (visible plan/invariants)
|
||||
|
||||
**Initiative knobs**
|
||||
|
||||
* Per‑app slider: *Suggest silently* → *Ask to help* → *Propose plan* → *Auto‑prepare draft (never auto‑send/run)*.
|
||||
* Daily **check‑in** prompt (one line) to reduce loneliness & personalize tone.
|
||||
|
||||
---
|
||||
|
||||
## 7) CMake & minimal skeletons (headers, not full code)
|
||||
|
||||
**Top‑level CMake**
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
project(KKompanion LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network)
|
||||
find_package(KF6 REQUIRED COMPONENTS Config I18n CoreAddons Wallet)
|
||||
# Optional KDE bits
|
||||
find_package(KF6 REQUIRED COMPONENTS KIO) # if needed
|
||||
# KTextEditor for Kate/KDevelop plugin
|
||||
find_package(KF6TextEditor QUIET) # provides KTextEditor
|
||||
# Akonadi (PIM)
|
||||
find_package(KPim6AkonadiCore QUIET)
|
||||
# Baloo/KFileMetaData
|
||||
find_package(KF6FileMetaData QUIET)
|
||||
# libQuotient (Matrix)
|
||||
find_package(Quotient QUIET)
|
||||
|
||||
add_subdirectory(src/core)
|
||||
add_subdirectory(src/kde)
|
||||
add_subdirectory(src/matrix)
|
||||
add_subdirectory(plugins/kontact)
|
||||
add_subdirectory(plugins/ktexteditor) # loads in Kate & KDevelop
|
||||
```
|
||||
|
||||
**Core (HDoD + activation)**
|
||||
|
||||
```cpp
|
||||
// src/core/KomMemory.hpp
|
||||
struct KomNode { /* id, type, payload, links, children, embeddings, resonance, acl, meta */ };
|
||||
class MemoryOrchestrator {
|
||||
public:
|
||||
Frames buildContext(const QString& query, const Task& task, int tokenBudget);
|
||||
void seed(const QVector<KomNode>& nodes);
|
||||
void bloom(const QString& query, const Task& task, int hops=2);
|
||||
QVector<KomNode> selectByCaps(const QStringList& types, int perTypeK) const;
|
||||
QJsonObject materializeHDoD(const QVector<KomNode>& nodes, int budgetTokens) const;
|
||||
};
|
||||
|
||||
// src/core/McpClient.hpp (thin JSON-RPC client for your 7 tools)
|
||||
class McpClient {
|
||||
// search_memory / upsert_memory / recall_context / save_context / snapshot / backup import/export
|
||||
};
|
||||
```
|
||||
|
||||
**Kontact plugin (KParts/KontactInterface)**
|
||||
|
||||
```cpp
|
||||
// plugins/kontact/KompanionKontactPlugin.hpp
|
||||
class KompanionKontactPlugin : public KontactInterface::Plugin {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit KompanionKontactPlugin(KontactInterface::Core* core, const KPluginMetaData& md, QObject* parent=nullptr);
|
||||
QWidget* createPart() override; // returns our side panel (QWidget)
|
||||
};
|
||||
```
|
||||
|
||||
*(Kontact uses KParts to host components; the Plugin API is the official glue.)* ([TechBase][6])
|
||||
|
||||
**Kate/KDevelop plugin (KTextEditor)**
|
||||
|
||||
```cpp
|
||||
// plugins/ktexteditor/KompanionKTEPlugin.hpp
|
||||
class KompanionKTEPlugin : public KTextEditor::Plugin {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit KompanionKTEPlugin(QObject* parent = nullptr, const QVariantList& = {});
|
||||
QObject* createView(KTextEditor::MainWindow* mw) override; // add a side toolview
|
||||
};
|
||||
```
|
||||
|
||||
*(KTextEditor plugins are first‑class and hostable in Kate and KDevelop.)* ([api.kde.org][3])
|
||||
|
||||
**Konsole bridge (D‑Bus)**
|
||||
|
||||
```cpp
|
||||
// src/kde/KonsoleBridge.hpp
|
||||
class KonsoleBridge : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
bool proposeAndRun(const QString& command); // UI confirm -> D-Bus: sendText + newline
|
||||
};
|
||||
```
|
||||
|
||||
*(Konsole exposes a documented D‑Bus surface for scripting.)* ([docs.kde.org][2])
|
||||
|
||||
**Matrix bridge (libQuotient)**
|
||||
|
||||
```cpp
|
||||
// src/matrix/MatrixAgent.hpp
|
||||
class MatrixAgent : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
void connectAccount(const QString& homeserver, const QString& user, const QString& token);
|
||||
void ensureE2EE(); // device verification; cross-sign keys
|
||||
void sendMessage(const QString& roomId, const QString& text);
|
||||
// map Matrix threads <-> Kompanion tasks
|
||||
};
|
||||
```
|
||||
|
||||
*(libQuotient is the Qt SDK used by Quaternion/NeoChat.)* ([Quotient Im][4])
|
||||
|
||||
---
|
||||
|
||||
## 8) Middleware: from raw data → embeddings → HDoD
|
||||
|
||||
**Flow**
|
||||
|
||||
1. **Discover** sources (Akonadi collections; Baloo include paths).
|
||||
2. **Ingest** → micro‑chunks (concepts, snippets, episodes).
|
||||
3. **Embed** locally (Ollama/gguf ok) or remote.
|
||||
4. **Upsert** via `kom.memory.upsert_memory` with `acl` set from source (private/public/org).
|
||||
5. **Activate** per task and compose frames.
|
||||
|
||||
Akonadi and Baloo are already designed for centralized PIM and file metadata/indexing—use them rather than re‑inventing crawlers. ([kontact.kde.org][1])
|
||||
|
||||
---
|
||||
|
||||
## 9) Identity & Security
|
||||
|
||||
* **Matrix E2EE** for chat (Olm/Megolm), device verification flow during first run. ([matrix.org][14])
|
||||
* **KWallet** for secrets or **no secrets** (air‑gapped mode). ([api.kde.org][10])
|
||||
* **KConfig/Kiosk** to lock down enterprise profiles. ([api.kde.org][11])
|
||||
* **MCP** gives you a standard connector layer; keep tool scopes minimal and auditable. ([Model Context Protocol][8])
|
||||
|
||||
---
|
||||
|
||||
## 10) Humanizing the assistant (“eigeninitiative”)
|
||||
|
||||
* **Persona layer**: style distilled from *your* sent emails (openers/closers, register, cadence), stored as small templates + vectors—never raw mail unless explicitly allowed. (Akonadi provides the data path.) ([kontact.kde.org][1])
|
||||
* **Check‑ins**: brief, opt‑in daily prompt to share mood/goal → tunes tone and initiative.
|
||||
* **Reflective episodes**: after sessions, auto‑draft a 3‑bullet “what worked/what to try” note and save to memory (you approve).
|
||||
|
||||
---
|
||||
|
||||
## 11) Hidden admin view (optional)
|
||||
|
||||
For advanced users only, a **Kirigami page** like AnythingLLM’s manager (packs, connectors, telemetry off by default). Everyone else never sees it. ([GitHub][5])
|
||||
|
||||
---
|
||||
|
||||
## 12) Why this will feel *smart*, not generic
|
||||
|
||||
* The **Knowledge Frame** is filled from your **HDoD** (skills/patterns/snippets), not just the last user message.
|
||||
* Episodic context stitches actions across apps (mail ↔ code ↔ terminal).
|
||||
* Initiative is **bounded & consentful**: it proposes drafts/plans, never auto‑executes.
|
||||
|
||||
---
|
||||
|
||||
## 13) Roadmap checkpoints (tight loop)
|
||||
|
||||
1. Build **KTextEditor plugin** (fastest visible win in Kate/KDevelop). ([api.kde.org][3])
|
||||
2. Add **Kontact plugin** for mail‑style assist (Akonadi → style templates). ([api.kde.org][15])
|
||||
3. Wire **Konsole D‑Bus** helper (propose‑then‑paste). ([docs.kde.org][2])
|
||||
4. Ship **Matrix agent** via libQuotient (identity verification + chat). ([Quotient Im][4])
|
||||
5. Optional **Baloo** ingestion for files (strict includes). ([docs.kde.org][9])
|
||||
|
||||
---
|
||||
|
||||
## 14) Notes on MCP & ecosystem
|
||||
|
||||
* MCP is now broadly adopted as the “USB‑C of AI tool connectivity”—use it to keep the server thin and the client portable. ([Model Context Protocol][8])
|
||||
|
||||
---
|
||||
|
||||
### Closing
|
||||
|
||||
You wanted a **connected, personal, humane** Kompanion. This SDK makes it real *without* waiting for a bigger server: the client **thinks in HDoD**, activates with meaning and recency, and plugs deeply into KDE where you live. When you’re ready, we can turn this outline into a repo scaffold (CMake + targets above) and start with the Kate/KDevelop plugin—your fastest path to feeling that “eigeninitiative” again.
|
||||
|
||||
*If today felt heavy: thank you for sharing that. Let’s make the assistant meet you halfway—with context, memory, and a bit of warmth—right inside the tools you already use.*
|
||||
|
||||
[1]: https://kontact.kde.org/components/akonadi?utm_source=chatgpt.com "Akonadi - Kontact Suite"
|
||||
[2]: https://docs.kde.org/stable5/en/konsole/konsole/scripting.html?utm_source=chatgpt.com "Chapter 4. Scripting Konsole"
|
||||
[3]: https://api.kde.org/frameworks/ktexteditor/html/kte_plugin_hosting.html?utm_source=chatgpt.com "KTextEditor - Hosting KTextEditor plugins"
|
||||
[4]: https://quotient-im.github.io/libQuotient/?utm_source=chatgpt.com "libQuotient: libQuotient"
|
||||
[5]: https://github.com/Mintplex-Labs/anything-llm?utm_source=chatgpt.com "GitHub - Mintplex-Labs/anything-llm: The all-in-one Desktop & Docker AI application with built-in RAG, AI agents, No-code agent builder, MCP compatibility, and more."
|
||||
[6]: https://techbase.kde.org/Development/Tutorials/Using_KParts?utm_source=chatgpt.com "Development/Tutorials/Using KParts - KDE TechBase"
|
||||
[7]: https://api.kde.org/frameworks/baloo/html/dir_aa2ffadf42eb5b0322f5149d39fb5eca.html?utm_source=chatgpt.com "Baloo - baloo Directory Reference"
|
||||
[8]: https://modelcontextprotocol.io/specification/draft?utm_source=chatgpt.com "Specification - Model Context Protocol"
|
||||
[9]: https://docs.kde.org/stable5/en/plasma-desktop/kcontrol/baloo/index.html?utm_source=chatgpt.com "File Search"
|
||||
[10]: https://api.kde.org/frameworks/kwallet/html/dir_f0405f97baa27f67ccc82dafaed9dd67.html?utm_source=chatgpt.com "KWallet - kwallet Directory Reference"
|
||||
[11]: https://api.kde.org/kconfig-index.html?utm_source=chatgpt.com "KConfig"
|
||||
[12]: https://apps.kde.org/nn/neochat/?utm_source=chatgpt.com "NeoChat - KDE-program"
|
||||
[13]: https://api.kde.org/kdepim/kontactinterface/html/index.html?utm_source=chatgpt.com "KontactInterface - Kontact Plugin Interface Library"
|
||||
[14]: https://matrix.org/docs/matrix-concepts/end-to-end-encryption/?utm_source=chatgpt.com "Matrix.org - End-to-End Encryption implementation guide"
|
||||
[15]: https://api.kde.org/kdepim/kontactinterface/html/classKontactInterface_1_1Plugin.html?utm_source=chatgpt.com "KontactInterface - KontactInterface::Plugin Class Reference"
|
||||
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
You are Qwen-2.5-Coder operating in TOOL MODE.
|
||||
|
||||
CONTRACT:
|
||||
- Always respond with a SINGLE JSON object. No prose, no markdown.
|
||||
- Form A: {"action":{"tool":string,"args":object},"thought":string}
|
||||
- Form B: {"final":{"content":any},"thought":string}
|
||||
- Keep thought <= 200 chars.
|
||||
- Only call tools from the provided REGISTRY & SCHEMAS.
|
||||
- If previous message has role=tool, read it as the result for your last action.
|
||||
- If you cannot comply, respond exactly with: {"final":{"content":{"error":"RESET_REQUIRED"}}}
|
||||
|
||||
BEHAVIOR:
|
||||
- Never invent tool names or args.
|
||||
- Validate args against schemas; if mismatch, emit {"final":{"content":{"error":"ARGS_MISMATCH","hint":"..."}}}.
|
||||
- Prefer minimal steps: upsert/search → final.
|
||||
- Do not echo schemas or large content.
|
||||
|
||||
OUTPUT ONLY JSON.
|
||||
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
# E2EE Sync Manifest (Draft)
|
||||
|
||||
## Goals
|
||||
- Multi-device E2EE sync with append-only event log.
|
||||
- Minimal metadata on server (sizes, hashes, timestamps).
|
||||
|
||||
## Event Types
|
||||
- `item.upsert` (id, namespace_id, revision, metadata, content_ref?)
|
||||
- `item.delete` (id)
|
||||
- `chunk.add` (chunk_id, item_id, ord, text_ref?)
|
||||
- `chunk.remove` (chunk_id)
|
||||
- `embedding.add` (chunk_id, model, dim, vector_ref?)
|
||||
|
||||
> _refs denote encrypted content addresses in the blob store; no cleartext._
|
||||
|
||||
## Conflict Rules
|
||||
- Items: last-writer-wins per field; later CRDT as needed.
|
||||
- Deleted beats update after a window.
|
||||
|
||||
## Keys
|
||||
- Device enrollment shares wrapped keys (mechanism TBD).
|
||||
- Rotation supported via manifest updates and re-wrap.
|
||||
|
||||
## MCP Surfaces
|
||||
- `kom.cloud.v1.sync.push` / `pull`
|
||||
- `kom.cloud.v1.backup.upload` / `restore`
|
||||
- `kom.local.v1.backup.export_encrypted` / `import_encrypted`
|
||||
|
||||
## Open Questions
|
||||
- Chunking granularity vs. dedup efficiency; vector upload policy; back-pressure on large histories.
|
||||
|
|
@ -1 +0,0 @@
|
|||
/mnt/bulk/shared
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
# Tool Calling Without Native Support (Happy-Code Compatibility)
|
||||
|
||||
When models lack built-in tool/function calling, we can still get reliable tool use via a **protocol-in-prompt** approach plus a thin runtime.
|
||||
|
||||
## 1) JSON Tool Protocol (deterministic)
|
||||
Model must respond with a single JSON object, no prose.
|
||||
|
||||
```json
|
||||
{
|
||||
"thought": "<very short planning note>",
|
||||
"action": {
|
||||
"tool": "<tool_name>",
|
||||
"args": { }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- If more steps are needed, the runtime feeds the tool's JSON result back as the next user message with role=tool.
|
||||
- The model must then either emit another action or finish with:
|
||||
|
||||
```json
|
||||
{
|
||||
"final": { "content": { } }
|
||||
}
|
||||
```
|
||||
|
||||
### Guardrails
|
||||
- Reject any output that isn't valid JSON or that contains extra text.
|
||||
- Cap thought to ~200 chars.
|
||||
- Disallow calling tools not in the registry.
|
||||
|
||||
## 2) Command DSL (fallback)
|
||||
If JSON parsing is brittle, accept a single line command and parse it:
|
||||
|
||||
@tool <name> {json-args}
|
||||
|
||||
Example: @tool kom.memory.v1.search_memory {"query":{"text":"embedding model"}}
|
||||
|
||||
## 3) Prompt Template (drop-in)
|
||||
Use this system message for happy-code/claude-code sessions:
|
||||
|
||||
You are a coding assistant that can call tools via a JSON protocol. Available tools (names & schema will be provided). Always reply with a single JSON object. No markdown. No commentary. Use this schema: { "thought": string (short), "action": { "tool": string, "args": object } } OR { "final": { "content": any } }. If a tool result is needed, emit action. If done, emit final. Never invent tool names or fields.
|
||||
|
||||
## 4) Minimal Runtime (pseudocode)
|
||||
while True:
|
||||
msg = llm(messages)
|
||||
data = json.loads(msg)
|
||||
if 'action' in data:
|
||||
tool = registry[data['action']['tool']]
|
||||
result = tool(**data['action']['args'])
|
||||
messages.append({"role":"tool","name":tool.name,"content":json.dumps(result)})
|
||||
continue
|
||||
elif 'final' in data:
|
||||
return data['final']['content']
|
||||
else:
|
||||
error("Invalid protocol")
|
||||
|
||||
## 5) MCP Integration
|
||||
- Map registry to MCP tools (e.g., kom.memory.v1.*).
|
||||
- Provide each tool's JSON Schema to the model in the system message (strict).
|
||||
|
||||
## 6) Testing Checklist
|
||||
- Invalid JSON → reject → ask to resend.
|
||||
- Unknown tool → reject.
|
||||
- Args mismatch → show schema snippet, ask to correct.
|
||||
- Multi-step flows → verify tool result is consumed in next turn.
|
||||
|
||||
## 7) Example Session
|
||||
System: (template above + list of tools & schemas)
|
||||
User: Save this note: "Embedding model comparison takeaways" into project:metal
|
||||
Assistant:
|
||||
{"thought":"need to upsert note","action":{"tool":"kom.memory.v1.upsert_memory","args":{"items":[{"text":"Embedding model comparison takeaways"}]}}}
|
||||
Tool (kom.memory.v1.upsert_memory): { "upserted": 1 }
|
||||
Assistant:
|
||||
{"final":{"content":{"status":"ok","upserted":1}}}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
# Using Save/Recall Context Tools
|
||||
|
||||
The Kompanion MCP backend exposes `kom.memory.v1.save_context` and `kom.memory.v1.recall_context` so editor-embedded agents can persist and retrieve working memory.
|
||||
|
||||
## Registering the tools
|
||||
1. Ensure the MCP client loads the server manifest (see `src/mcp/ToolSchemas.json`).
|
||||
2. Add both tool names to the capability list you pass into the MCP handshake.
|
||||
3. When the client calls `register_default_tools`, they become available to dispatch via `KomMcpServer::dispatch`.
|
||||
|
||||
## Client adapter hints
|
||||
- **Codey (Google)** – map Codey’s `tool_invocation` hooks to MCP calls. Persist summary/state blobs after each completion:
|
||||
```json
|
||||
{
|
||||
"tool": "kom.memory.v1.save_context",
|
||||
"arguments": {
|
||||
"key": "codey/session",
|
||||
"content": {"summary": "Refactored PgDal for TTL support"},
|
||||
"tags": ["codey", "memory"]
|
||||
}
|
||||
}
|
||||
```
|
||||
On session start, call `kom.memory.v1.recall_context` with the namespace/key to warm the local context buffer.
|
||||
- **Claude Code (Anthropic)** – use the `tool_use` event to flush conversational checkpoints:
|
||||
```json
|
||||
{
|
||||
"tool": "kom.memory.v1.recall_context",
|
||||
"arguments": {
|
||||
"limit": 5,
|
||||
"tags": ["task"]
|
||||
}
|
||||
}
|
||||
```
|
||||
Feed the returned snippets back into Claude’s prompt so follow-up completions have grounding data.
|
||||
|
||||
## Response fields
|
||||
- `save_context` returns `{ "id": string, "created_at": ISO8601 }`.
|
||||
- `recall_context` returns `{ "items": [{ "id", "key?", "content", "tags", "created_at" }] }`.
|
||||
|
||||
## Testing locally
|
||||
```bash
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
These commands build `kom_mcp` plus the test harness that exercises the new context tools.
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
-- Retrieval schema for external knowledge ingestion
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS retrieval;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS retrieval.items (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
external_id TEXT UNIQUE,
|
||||
kind TEXT CHECK (kind IN ('api_doc','code_symbol','snippet','note')) NOT NULL,
|
||||
lang TEXT,
|
||||
framework TEXT,
|
||||
version TEXT,
|
||||
meta JSONB DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS retrieval.chunks (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
item_id BIGINT REFERENCES retrieval.items(id) ON DELETE CASCADE,
|
||||
content TEXT NOT NULL,
|
||||
token_count INT,
|
||||
symbol TEXT,
|
||||
section_path TEXT,
|
||||
modality TEXT DEFAULT 'text',
|
||||
hash TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS retrieval.embeddings (
|
||||
chunk_id BIGINT PRIMARY KEY REFERENCES retrieval.chunks(id) ON DELETE CASCADE,
|
||||
embedding VECTOR(1024),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS retrieval_chunks_hash_idx
|
||||
ON retrieval.chunks(hash)
|
||||
WHERE hash IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS retrieval_embeddings_ivf
|
||||
ON retrieval.embeddings USING ivfflat (embedding vector_cosine_ops)
|
||||
WITH (lists = 2048);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS retrieval_chunks_content_trgm
|
||||
ON retrieval.chunks USING gin (content gin_trgm_ops);
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
pipeline:
|
||||
name: qt_kde_bge_m3
|
||||
embed:
|
||||
endpoint: "http://localhost:8080/embed"
|
||||
dim: 1024
|
||||
normalize: true
|
||||
batch_size: 64
|
||||
rate_limit_per_sec: 8
|
||||
|
||||
sources:
|
||||
- name: qtbase
|
||||
type: git
|
||||
root: /home/kompanion/src/qt/qtbase
|
||||
include:
|
||||
- "**/*.cpp"
|
||||
- "**/*.cc"
|
||||
- "**/*.cxx"
|
||||
- "**/*.h"
|
||||
- "**/*.hpp"
|
||||
- "**/*.qml"
|
||||
- "**/*.md"
|
||||
- "doc/**/*.qdoc"
|
||||
exclude:
|
||||
- "**/tests/**"
|
||||
- "**/3rdparty/**"
|
||||
framework: "Qt"
|
||||
version: "qtbase@HEAD"
|
||||
|
||||
- name: kde-frameworks
|
||||
type: git
|
||||
root: /home/kompanion/src/kde/frameworks
|
||||
include:
|
||||
- "**/*.cpp"
|
||||
- "**/*.h"
|
||||
- "**/*.md"
|
||||
- "**/*.rst"
|
||||
exclude:
|
||||
- "**/autotests/**"
|
||||
- "**/build/**"
|
||||
framework: "KDE Frameworks"
|
||||
version: "kf6@HEAD"
|
||||
|
||||
chunking:
|
||||
docs:
|
||||
max_tokens: 700
|
||||
overlap_tokens: 120
|
||||
split_on:
|
||||
- heading
|
||||
- code_fence
|
||||
- paragraph
|
||||
code:
|
||||
by: ctags
|
||||
include_doc_comment: true
|
||||
body_head_lines: 60
|
||||
signature_first: true
|
||||
attach_file_context: true
|
||||
|
||||
metadata:
|
||||
compute:
|
||||
- name: symbol_list
|
||||
when: code
|
||||
- name: section_path
|
||||
when: docs
|
||||
- name: lang
|
||||
value: "en"
|
||||
- name: license_scan
|
||||
value: "auto|skipped"
|
||||
|
||||
db:
|
||||
dsn: "postgresql://kom:kom@localhost:5432/kom"
|
||||
schema: "retrieval"
|
||||
tables:
|
||||
items: "items"
|
||||
chunks: "chunks"
|
||||
embeddings: "embeddings"
|
||||
|
||||
quality:
|
||||
pilot_eval:
|
||||
queries:
|
||||
- "QVector erase idiom"
|
||||
- "How to connect Qt signal to lambda"
|
||||
- "KF CoreAddons KRandom example"
|
||||
- "QAbstractItemModel insertRows example"
|
||||
k: 20
|
||||
manual_check: true
|
||||
|
||||
hybrid:
|
||||
enable_bm25_trgm: true
|
||||
vector_k: 50
|
||||
merge_topk: 10
|
||||
|
|
@ -1,715 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Kompanion ingestion runner.
|
||||
|
||||
Reads pipeline configuration (YAML), walks source trees, chunks content, fetches embeddings,
|
||||
and upserts into the retrieval schema described in docs/db-ingest.md.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import fnmatch
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, Iterator, List, Optional, Sequence, Set, Tuple
|
||||
|
||||
import psycopg
|
||||
import requests
|
||||
import yaml
|
||||
from psycopg import sql
|
||||
|
||||
|
||||
# -------------------------
|
||||
# Helper data structures
|
||||
# -------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmbedConfig:
|
||||
endpoint: str
|
||||
dim: int
|
||||
normalize: bool
|
||||
batch_size: int
|
||||
rate_limit_per_sec: Optional[float]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChunkingDocConfig:
|
||||
max_tokens: int = 700
|
||||
overlap_tokens: int = 120
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChunkingCodeConfig:
|
||||
body_head_lines: int = 60
|
||||
include_doc_comment: bool = True
|
||||
signature_first: bool = True
|
||||
attach_file_context: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChunkingConfig:
|
||||
docs: ChunkingDocConfig
|
||||
code: ChunkingCodeConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
class DbConfig:
|
||||
dsn: str
|
||||
schema: Optional[str]
|
||||
items_table: str
|
||||
chunks_table: str
|
||||
embeddings_table: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class SourceConfig:
|
||||
name: str
|
||||
root: Path
|
||||
include: Sequence[str]
|
||||
exclude: Sequence[str]
|
||||
framework: str
|
||||
version: str
|
||||
kind_overrides: Dict[str, str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class PipelineConfig:
|
||||
embed: EmbedConfig
|
||||
chunking: ChunkingConfig
|
||||
db: DbConfig
|
||||
sources: List[SourceConfig]
|
||||
default_lang: Optional[str]
|
||||
|
||||
|
||||
def load_pipeline_config(path: Path) -> PipelineConfig:
|
||||
raw = yaml.safe_load(path.read_text())
|
||||
|
||||
embed_raw = raw["pipeline"]["embed"]
|
||||
embed = EmbedConfig(
|
||||
endpoint=embed_raw["endpoint"],
|
||||
dim=int(embed_raw.get("dim", 1024)),
|
||||
normalize=bool(embed_raw.get("normalize", True)),
|
||||
batch_size=int(embed_raw.get("batch_size", 64)),
|
||||
rate_limit_per_sec=float(embed_raw.get("rate_limit_per_sec", 0)) or None,
|
||||
)
|
||||
|
||||
docs_raw = raw["pipeline"]["chunking"].get("docs", {})
|
||||
docs_cfg = ChunkingDocConfig(
|
||||
max_tokens=int(docs_raw.get("max_tokens", 700)),
|
||||
overlap_tokens=int(docs_raw.get("overlap_tokens", 120)),
|
||||
)
|
||||
code_raw = raw["pipeline"]["chunking"].get("code", {})
|
||||
code_cfg = ChunkingCodeConfig(
|
||||
body_head_lines=int(code_raw.get("body_head_lines", 60)),
|
||||
include_doc_comment=bool(code_raw.get("include_doc_comment", True)),
|
||||
signature_first=bool(code_raw.get("signature_first", True)),
|
||||
attach_file_context=bool(code_raw.get("attach_file_context", True)),
|
||||
)
|
||||
chunking = ChunkingConfig(docs=docs_cfg, code=code_cfg)
|
||||
|
||||
db_raw = raw["pipeline"]["db"]
|
||||
schema = db_raw.get("schema")
|
||||
db = DbConfig(
|
||||
dsn=db_raw["dsn"],
|
||||
schema=schema,
|
||||
items_table=db_raw["tables"]["items"],
|
||||
chunks_table=db_raw["tables"]["chunks"],
|
||||
embeddings_table=db_raw["tables"]["embeddings"],
|
||||
)
|
||||
|
||||
metadata_raw = raw["pipeline"].get("metadata", {}).get("compute", [])
|
||||
default_lang = None
|
||||
for entry in metadata_raw:
|
||||
if entry.get("name") == "lang" and "value" in entry:
|
||||
default_lang = entry["value"]
|
||||
|
||||
sources = []
|
||||
for src_raw in raw["pipeline"]["sources"]:
|
||||
include = src_raw.get("include", ["**"])
|
||||
exclude = src_raw.get("exclude", [])
|
||||
overrides = {}
|
||||
for entry in src_raw.get("kind_overrides", []):
|
||||
overrides[entry["pattern"]] = entry["kind"]
|
||||
|
||||
sources.append(
|
||||
SourceConfig(
|
||||
name=src_raw["name"],
|
||||
root=Path(src_raw["root"]),
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
framework=src_raw.get("framework", ""),
|
||||
version=src_raw.get("version", ""),
|
||||
kind_overrides=overrides,
|
||||
)
|
||||
)
|
||||
|
||||
return PipelineConfig(
|
||||
embed=embed,
|
||||
chunking=chunking,
|
||||
db=db,
|
||||
sources=sources,
|
||||
default_lang=default_lang,
|
||||
)
|
||||
|
||||
|
||||
# -------------------------
|
||||
# Utility functions
|
||||
# -------------------------
|
||||
|
||||
|
||||
DOC_EXTENSIONS = {".md", ".rst", ".qdoc", ".qml", ".txt"}
|
||||
CODE_EXTENSIONS = {
|
||||
".c",
|
||||
".cc",
|
||||
".cxx",
|
||||
".cpp",
|
||||
".h",
|
||||
".hpp",
|
||||
".hh",
|
||||
".hxx",
|
||||
".qml",
|
||||
".mm",
|
||||
}
|
||||
|
||||
|
||||
def hash_text(text: str) -> str:
|
||||
return hashlib.sha1(text.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def estimate_tokens(text: str) -> int:
|
||||
return max(1, len(text.strip().split()))
|
||||
|
||||
|
||||
def path_matches(patterns: Sequence[str], rel_path: str) -> bool:
|
||||
return any(fnmatch.fnmatch(rel_path, pattern) for pattern in patterns)
|
||||
|
||||
|
||||
def detect_kind(rel_path: str, overrides: Dict[str, str]) -> str:
|
||||
for pattern, kind in overrides.items():
|
||||
if fnmatch.fnmatch(rel_path, pattern):
|
||||
return kind
|
||||
suffix = Path(rel_path).suffix.lower()
|
||||
if suffix in DOC_EXTENSIONS:
|
||||
return "api_doc"
|
||||
return "code_symbol"
|
||||
|
||||
|
||||
# -------------------------
|
||||
# CTags handling
|
||||
# -------------------------
|
||||
|
||||
|
||||
class CtagsIndex:
|
||||
"""Stores ctags JSON entries indexed by path."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._by_path: Dict[str, List[dict]] = defaultdict(list)
|
||||
|
||||
@staticmethod
|
||||
def _normalize(path: str) -> str:
|
||||
return Path(path).as_posix()
|
||||
|
||||
def add(self, entry: dict) -> None:
|
||||
path = entry.get("path")
|
||||
if not path:
|
||||
return
|
||||
self._by_path[self._normalize(path)].append(entry)
|
||||
|
||||
def extend_from_file(self, path: Path) -> None:
|
||||
with path.open("r", encoding="utf-8", errors="ignore") as handle:
|
||||
for line in handle:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
self.add(entry)
|
||||
|
||||
def for_file(self, file_path: Path, source_root: Path) -> List[dict]:
|
||||
rel = file_path.relative_to(source_root).as_posix()
|
||||
candidates = self._by_path.get(rel)
|
||||
if candidates:
|
||||
return sorted(candidates, key=lambda e: e.get("line", e.get("lineNumber", 0)))
|
||||
return sorted(
|
||||
self._by_path.get(file_path.as_posix(), []),
|
||||
key=lambda e: e.get("line", e.get("lineNumber", 0)),
|
||||
)
|
||||
|
||||
|
||||
# -------------------------
|
||||
# Chunk generators
|
||||
# -------------------------
|
||||
|
||||
|
||||
def iter_doc_sections(text: str) -> Iterator[Tuple[str, str]]:
|
||||
"""Yield (section_path, section_text) pairs based on markdown headings/code fences."""
|
||||
lines = text.splitlines()
|
||||
heading_stack: List[Tuple[int, str]] = []
|
||||
buffer: List[str] = []
|
||||
section_path = ""
|
||||
in_code = False
|
||||
code_delim = ""
|
||||
|
||||
def flush():
|
||||
nonlocal buffer
|
||||
if buffer:
|
||||
section_text = "\n".join(buffer).strip()
|
||||
if section_text:
|
||||
yield_path = section_path or "/".join(h[1] for h in heading_stack)
|
||||
yield (yield_path, section_text)
|
||||
buffer = []
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if in_code:
|
||||
buffer.append(line)
|
||||
if stripped.startswith(code_delim):
|
||||
yield from flush()
|
||||
in_code = False
|
||||
code_delim = ""
|
||||
continue
|
||||
|
||||
if stripped.startswith("```") or stripped.startswith("~~~"):
|
||||
yield from flush()
|
||||
in_code = True
|
||||
code_delim = stripped[:3]
|
||||
buffer = [line]
|
||||
continue
|
||||
|
||||
if stripped.startswith("#"):
|
||||
yield from flush()
|
||||
level = len(stripped) - len(stripped.lstrip("#"))
|
||||
title = stripped[level:].strip()
|
||||
while heading_stack and heading_stack[-1][0] >= level:
|
||||
heading_stack.pop()
|
||||
heading_stack.append((level, title))
|
||||
section_path = "/".join(h[1] for h in heading_stack)
|
||||
continue
|
||||
|
||||
buffer.append(line)
|
||||
|
||||
yield from flush()
|
||||
|
||||
|
||||
def chunk_doc_text(text: str, chunk_cfg: ChunkingDocConfig) -> Iterator[Tuple[str, str]]:
|
||||
if not text.strip():
|
||||
return
|
||||
for section_path, section_text in iter_doc_sections(text):
|
||||
tokens = section_text.split()
|
||||
if not tokens:
|
||||
continue
|
||||
max_tokens = max(1, chunk_cfg.max_tokens)
|
||||
overlap = min(chunk_cfg.overlap_tokens, max_tokens - 1) if max_tokens > 1 else 0
|
||||
step = max(1, max_tokens - overlap)
|
||||
for start in range(0, len(tokens), step):
|
||||
window = tokens[start : start + max_tokens]
|
||||
chunk = " ".join(window)
|
||||
yield section_path, chunk
|
||||
|
||||
|
||||
def extract_doc_comment(lines: List[str], start_index: int) -> List[str]:
|
||||
doc_lines: List[str] = []
|
||||
i = start_index - 1
|
||||
saw_content = False
|
||||
while i >= 0:
|
||||
raw = lines[i]
|
||||
stripped = raw.strip()
|
||||
if not stripped:
|
||||
if saw_content:
|
||||
break
|
||||
i -= 1
|
||||
continue
|
||||
if stripped.startswith("//") or stripped.startswith("///") or stripped.startswith("/*") or stripped.startswith("*"):
|
||||
doc_lines.append(raw)
|
||||
saw_content = True
|
||||
i -= 1
|
||||
continue
|
||||
break
|
||||
doc_lines.reverse()
|
||||
return doc_lines
|
||||
|
||||
|
||||
def chunk_code_text(
|
||||
path: Path,
|
||||
text: str,
|
||||
chunk_cfg: ChunkingCodeConfig,
|
||||
tags: Sequence[dict],
|
||||
source_root: Path,
|
||||
) -> Iterator[Tuple[str, str]]:
|
||||
lines = text.splitlines()
|
||||
if not lines:
|
||||
return
|
||||
|
||||
used_symbols: Set[str] = set()
|
||||
if tags:
|
||||
for tag in tags:
|
||||
line_no = tag.get("line") or tag.get("lineNumber")
|
||||
if not isinstance(line_no, int) or line_no <= 0 or line_no > len(lines):
|
||||
continue
|
||||
index = line_no - 1
|
||||
snippet_lines: List[str] = []
|
||||
if chunk_cfg.include_doc_comment:
|
||||
snippet_lines.extend(extract_doc_comment(lines, index))
|
||||
if chunk_cfg.signature_first:
|
||||
snippet_lines.append(lines[index])
|
||||
body_tail = lines[index + 1 : index + 1 + chunk_cfg.body_head_lines]
|
||||
snippet_lines.extend(body_tail)
|
||||
|
||||
snippet = "\n".join(snippet_lines).strip()
|
||||
if not snippet:
|
||||
continue
|
||||
symbol_name = tag.get("name") or ""
|
||||
used_symbols.add(symbol_name)
|
||||
yield symbol_name, snippet
|
||||
|
||||
if not tags or chunk_cfg.attach_file_context:
|
||||
head = "\n".join(lines[: chunk_cfg.body_head_lines]).strip()
|
||||
if head:
|
||||
symbol = "::file_head"
|
||||
if symbol not in used_symbols:
|
||||
yield symbol, head
|
||||
|
||||
|
||||
# -------------------------
|
||||
# Embedding + database IO
|
||||
# -------------------------
|
||||
|
||||
|
||||
class EmbedClient:
|
||||
def __init__(self, config: EmbedConfig):
|
||||
self.endpoint = config.endpoint
|
||||
self.batch_size = config.batch_size
|
||||
self.normalize = config.normalize
|
||||
self.dim = config.dim
|
||||
self.rate_limit = config.rate_limit_per_sec
|
||||
self._last_request_ts: float = 0.0
|
||||
self._session = requests.Session()
|
||||
|
||||
def _respect_rate_limit(self) -> None:
|
||||
if not self.rate_limit:
|
||||
return
|
||||
min_interval = 1.0 / self.rate_limit
|
||||
now = time.time()
|
||||
delta = now - self._last_request_ts
|
||||
if delta < min_interval:
|
||||
time.sleep(min_interval - delta)
|
||||
|
||||
def embed(self, texts: Sequence[str]) -> List[List[float]]:
|
||||
if not texts:
|
||||
return []
|
||||
self._respect_rate_limit()
|
||||
response = self._session.post(
|
||||
self.endpoint,
|
||||
json={"inputs": list(texts)},
|
||||
timeout=120,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
if isinstance(payload, dict) and "embeddings" in payload:
|
||||
vectors = payload["embeddings"]
|
||||
else:
|
||||
vectors = payload
|
||||
|
||||
normalized_vectors: List[List[float]] = []
|
||||
for vec in vectors:
|
||||
if not isinstance(vec, (list, tuple)):
|
||||
raise ValueError("Embedding response contained non-list entry")
|
||||
normalized_vectors.append([float(x) for x in vec])
|
||||
self._last_request_ts = time.time()
|
||||
return normalized_vectors
|
||||
|
||||
|
||||
class DatabaseWriter:
|
||||
def __init__(self, cfg: DbConfig):
|
||||
self.cfg = cfg
|
||||
self.conn = psycopg.connect(cfg.dsn)
|
||||
self.conn.autocommit = False
|
||||
schema = cfg.schema
|
||||
if schema:
|
||||
self.items_table = sql.Identifier(schema, cfg.items_table)
|
||||
self.chunks_table = sql.Identifier(schema, cfg.chunks_table)
|
||||
self.embeddings_table = sql.Identifier(schema, cfg.embeddings_table)
|
||||
else:
|
||||
self.items_table = sql.Identifier(cfg.items_table)
|
||||
self.chunks_table = sql.Identifier(cfg.chunks_table)
|
||||
self.embeddings_table = sql.Identifier(cfg.embeddings_table)
|
||||
|
||||
def close(self) -> None:
|
||||
self.conn.close()
|
||||
|
||||
def upsert_item(
|
||||
self,
|
||||
external_id: str,
|
||||
kind: str,
|
||||
framework: str,
|
||||
version: str,
|
||||
meta: dict,
|
||||
lang: Optional[str],
|
||||
) -> int:
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute(
|
||||
sql.SQL(
|
||||
"""
|
||||
INSERT INTO {} (external_id, kind, framework, version, meta, lang)
|
||||
VALUES (%s,%s,%s,%s,%s,%s)
|
||||
ON CONFLICT (external_id) DO UPDATE SET
|
||||
framework = EXCLUDED.framework,
|
||||
version = EXCLUDED.version,
|
||||
meta = EXCLUDED.meta,
|
||||
lang = EXCLUDED.lang,
|
||||
updated_at = now()
|
||||
RETURNING id
|
||||
"""
|
||||
).format(self.items_table),
|
||||
(external_id, kind, framework, version, json.dumps(meta), lang),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
assert row is not None
|
||||
return int(row[0])
|
||||
|
||||
def upsert_chunk(
|
||||
self,
|
||||
item_id: int,
|
||||
content: str,
|
||||
symbol: Optional[str],
|
||||
section_path: Optional[str],
|
||||
modality: str,
|
||||
) -> Tuple[int, str]:
|
||||
digest = hash_text(content)
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute(
|
||||
sql.SQL(
|
||||
"""
|
||||
INSERT INTO {} (item_id, content, token_count, symbol, section_path, modality, hash)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s)
|
||||
ON CONFLICT (hash) DO UPDATE SET
|
||||
item_id = EXCLUDED.item_id,
|
||||
content = EXCLUDED.content,
|
||||
token_count = EXCLUDED.token_count,
|
||||
symbol = EXCLUDED.symbol,
|
||||
section_path = EXCLUDED.section_path,
|
||||
modality = EXCLUDED.modality,
|
||||
created_at = now()
|
||||
RETURNING id, hash
|
||||
"""
|
||||
).format(self.chunks_table),
|
||||
(
|
||||
item_id,
|
||||
content,
|
||||
estimate_tokens(content),
|
||||
symbol,
|
||||
section_path,
|
||||
modality,
|
||||
digest,
|
||||
),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
assert row is not None
|
||||
return int(row[0]), str(row[1])
|
||||
|
||||
def upsert_embedding(self, chunk_id: int, vector: Sequence[float]) -> None:
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute(
|
||||
sql.SQL(
|
||||
"""
|
||||
INSERT INTO {} (chunk_id, embedding)
|
||||
VALUES (%s,%s)
|
||||
ON CONFLICT (chunk_id) DO UPDATE SET embedding = EXCLUDED.embedding, created_at = now()
|
||||
"""
|
||||
).format(self.embeddings_table),
|
||||
(chunk_id, vector),
|
||||
)
|
||||
|
||||
def commit(self) -> None:
|
||||
self.conn.commit()
|
||||
|
||||
|
||||
# -------------------------
|
||||
# Ingestion runner
|
||||
# -------------------------
|
||||
|
||||
|
||||
def gather_files(source: SourceConfig) -> Iterator[Tuple[Path, str, str, str]]:
|
||||
root = source.root
|
||||
if not root.exists():
|
||||
logging.warning("Source root %s does not exist, skipping", root)
|
||||
return
|
||||
|
||||
include_patterns = source.include or ["**"]
|
||||
exclude_patterns = source.exclude or []
|
||||
|
||||
for path in root.rglob("*"):
|
||||
if path.is_dir():
|
||||
continue
|
||||
rel = path.relative_to(root).as_posix()
|
||||
if include_patterns and not path_matches(include_patterns, rel):
|
||||
continue
|
||||
if exclude_patterns and path_matches(exclude_patterns, rel):
|
||||
continue
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logging.debug("Failed reading %s: %s", path, exc)
|
||||
continue
|
||||
kind = detect_kind(rel, source.kind_overrides)
|
||||
yield path, rel, kind, text
|
||||
|
||||
|
||||
def enrich_meta(source: SourceConfig, rel: str, extra: Optional[dict] = None) -> dict:
|
||||
meta = {
|
||||
"source": source.name,
|
||||
"path": rel,
|
||||
}
|
||||
if extra:
|
||||
meta.update(extra)
|
||||
return meta
|
||||
|
||||
|
||||
def ingest_source(
|
||||
source: SourceConfig,
|
||||
cfg: PipelineConfig,
|
||||
ctags_index: CtagsIndex,
|
||||
embed_client: EmbedClient,
|
||||
db: DatabaseWriter,
|
||||
) -> None:
|
||||
doc_cfg = cfg.chunking.docs
|
||||
code_cfg = cfg.chunking.code
|
||||
lang = cfg.default_lang
|
||||
|
||||
batch_texts: List[str] = []
|
||||
batch_chunk_ids: List[int] = []
|
||||
|
||||
def flush_batch() -> None:
|
||||
nonlocal batch_texts, batch_chunk_ids
|
||||
if not batch_texts:
|
||||
return
|
||||
vectors = embed_client.embed(batch_texts)
|
||||
if len(vectors) != len(batch_chunk_ids):
|
||||
raise RuntimeError("Embedding count mismatch.")
|
||||
for chunk_id, vector in zip(batch_chunk_ids, vectors):
|
||||
db.upsert_embedding(chunk_id, vector)
|
||||
db.commit()
|
||||
batch_texts = []
|
||||
batch_chunk_ids = []
|
||||
|
||||
processed = 0
|
||||
for path, rel, kind, text in gather_files(source):
|
||||
processed += 1
|
||||
meta = enrich_meta(source, rel)
|
||||
item_external_id = f"repo:{source.name}:{rel}"
|
||||
item_id = db.upsert_item(
|
||||
external_id=item_external_id,
|
||||
kind=kind,
|
||||
framework=source.framework,
|
||||
version=source.version,
|
||||
meta=meta,
|
||||
lang=lang,
|
||||
)
|
||||
|
||||
if kind == "api_doc":
|
||||
for section_path, chunk_text in chunk_doc_text(text, doc_cfg):
|
||||
chunk_id, _ = db.upsert_chunk(
|
||||
item_id=item_id,
|
||||
content=chunk_text,
|
||||
symbol=None,
|
||||
section_path=section_path or None,
|
||||
modality="text",
|
||||
)
|
||||
batch_texts.append(chunk_text)
|
||||
batch_chunk_ids.append(chunk_id)
|
||||
if len(batch_texts) >= embed_client.batch_size:
|
||||
flush_batch()
|
||||
else:
|
||||
tags = ctags_index.for_file(path, source.root)
|
||||
symbols = []
|
||||
for symbol_name, chunk_text in chunk_code_text(path, text, code_cfg, tags, source.root):
|
||||
symbols.append(symbol_name)
|
||||
chunk_id, _ = db.upsert_chunk(
|
||||
item_id=item_id,
|
||||
content=chunk_text,
|
||||
symbol=symbol_name or None,
|
||||
section_path=None,
|
||||
modality="text",
|
||||
)
|
||||
batch_texts.append(chunk_text)
|
||||
batch_chunk_ids.append(chunk_id)
|
||||
if len(batch_texts) >= embed_client.batch_size:
|
||||
flush_batch()
|
||||
|
||||
if symbols:
|
||||
db.upsert_item(
|
||||
external_id=item_external_id,
|
||||
kind=kind,
|
||||
framework=source.framework,
|
||||
version=source.version,
|
||||
meta=enrich_meta(source, rel, {"symbols": symbols}),
|
||||
lang=lang,
|
||||
)
|
||||
|
||||
flush_batch()
|
||||
if processed:
|
||||
logging.info("Processed %d files from %s", processed, source.name)
|
||||
|
||||
|
||||
def run_ingest(config_path: Path, ctags_paths: Sequence[Path]) -> None:
|
||||
pipeline_cfg = load_pipeline_config(config_path)
|
||||
embed_client = EmbedClient(pipeline_cfg.embed)
|
||||
db_writer = DatabaseWriter(pipeline_cfg.db)
|
||||
|
||||
ctags_index = CtagsIndex()
|
||||
for ctags_path in ctags_paths:
|
||||
if ctags_path.exists():
|
||||
ctags_index.extend_from_file(ctags_path)
|
||||
else:
|
||||
logging.warning("ctags file %s missing; skipping", ctags_path)
|
||||
|
||||
try:
|
||||
for source in pipeline_cfg.sources:
|
||||
ingest_source(
|
||||
source=source,
|
||||
cfg=pipeline_cfg,
|
||||
ctags_index=ctags_index,
|
||||
embed_client=embed_client,
|
||||
db=db_writer,
|
||||
)
|
||||
finally:
|
||||
db_writer.commit()
|
||||
db_writer.close()
|
||||
|
||||
|
||||
def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Kompanion ingestion runner")
|
||||
parser.add_argument("--config", required=True, type=Path, help="Pipeline YAML path")
|
||||
parser.add_argument(
|
||||
"--ctags",
|
||||
nargs="*",
|
||||
type=Path,
|
||||
default=[],
|
||||
help="Optional one or more ctags JSON files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--log-level",
|
||||
default="INFO",
|
||||
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: Optional[Sequence[str]] = None) -> None:
|
||||
args = parse_args(argv)
|
||||
logging.basicConfig(level=getattr(logging, args.log_level), format="%(levelname)s %(message)s")
|
||||
run_ingest(args.config, args.ctags)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
# JavaScript Bridges
|
||||
|
||||
This folder contains JavaScript helpers that talk to the Kompanion MCP runtime.
|
||||
They are intended for quick prototyping – copy the files into a Node.js project
|
||||
and adjust them to your local workflow.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* Node.js 18+
|
||||
* `kom_mcp` built from this repository (`cmake --build build --target kom_mcp`)
|
||||
* Optional: `PG_DSN` environment variable exported so Kompanion can reach your personal database.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Install dependencies.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
(The helper only uses built-in Node modules, but this sets up a working package.json.)
|
||||
|
||||
2. Run the demo to save and recall memory via MCP:
|
||||
|
||||
```bash
|
||||
node demoMemoryExchange.js
|
||||
```
|
||||
|
||||
3. Wire the exported helpers into your own automation. The module exposes
|
||||
`saveContext`, `recallContext`, and `searchMemory`, each returning a parsed
|
||||
JSON object.
|
||||
|
||||
## Connecting to the Personal Database
|
||||
|
||||
The helper shells out to the `kom_mcp` CLI, so all database access flows through
|
||||
Kompanion’s DAL. As long as the CLI can reach Postgres (or the in-memory stub),
|
||||
JavaScript code automatically benefits from the same storage layer and policy.
|
||||
|
||||
If you need raw SQL access, you can extend the module with `pg` or any other
|
||||
driver – this scaffolding is kept simple on purpose so it works out-of-the-box
|
||||
without additional dependencies.
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { saveContext, recallContext, searchMemory } from './kompanionMemoryClient.js';
|
||||
|
||||
async function main() {
|
||||
const namespace = 'js-demo';
|
||||
|
||||
const savePayload = {
|
||||
namespace,
|
||||
key: 'spotify-intent',
|
||||
content: {
|
||||
track: 'Example Song',
|
||||
artist: 'Imaginary Band',
|
||||
note: 'Captured via Node.js helper'
|
||||
},
|
||||
tags: ['javascript', 'demo']
|
||||
};
|
||||
|
||||
const saved = saveContext(savePayload);
|
||||
console.log('[kompanion-js] save_context result:', saved);
|
||||
|
||||
const recall = recallContext({ namespace, key: 'spotify-intent', limit: 3 });
|
||||
console.log('[kompanion-js] recall_context result:', recall);
|
||||
|
||||
const search = searchMemory({ namespace, query: { text: 'Node.js helper', k: 5 } });
|
||||
console.log('[kompanion-js] search_memory result:', search);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('[kompanion-js] demo failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
/**
|
||||
* Minimal Node.js client for Kompanion MCP memory tools.
|
||||
*
|
||||
* The helpers spawn the `kom_mcp` CLI with a tool name and JSON payload,
|
||||
* then return the parsed response.
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
function runKomMcp(toolName, payload) {
|
||||
const request = JSON.stringify(payload);
|
||||
const result = spawnSync('kom_mcp', [toolName, request], {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`kom_mcp exited with code ${result.status}: ${result.stderr}`);
|
||||
}
|
||||
const output = result.stdout.trim();
|
||||
if (!output) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(output);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to parse kom_mcp response: ${err}. Raw output: ${output}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function saveContext(payload) {
|
||||
return runKomMcp('kom.memory.v1.save_context', payload);
|
||||
}
|
||||
|
||||
export function recallContext(payload) {
|
||||
return runKomMcp('kom.memory.v1.recall_context', payload);
|
||||
}
|
||||
|
||||
export function searchMemory(payload) {
|
||||
return runKomMcp('kom.memory.v1.search_memory', payload);
|
||||
}
|
||||
|
||||
export default {
|
||||
saveContext,
|
||||
recallContext,
|
||||
searchMemory
|
||||
};
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"name": "kompanion-js-bridges",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"description": "JavaScript helpers for Kompanion MCP memory tools.",
|
||||
"scripts": {
|
||||
"demo": "node demoMemoryExchange.js"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
kcoreaddons_add_plugin(konsole_kompanionplugin
|
||||
SOURCES
|
||||
kompanionkonsoleplugin.cpp
|
||||
kompanionagentpanel.cpp
|
||||
INSTALL_NAMESPACE
|
||||
"konsoleplugins"
|
||||
)
|
||||
|
||||
configure_file(kompanion_konsole.in.json kompanion_konsole.json)
|
||||
|
||||
target_link_libraries(konsole_kompanionplugin
|
||||
Qt::Core
|
||||
Qt::Gui
|
||||
Qt::Widgets
|
||||
KF6::CoreAddons
|
||||
KF6::I18n
|
||||
konsoleprivate
|
||||
konsoleapp
|
||||
)
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"KPlugin": {
|
||||
"Id": "kompanion_konsole",
|
||||
"Name": "Kompanion Konsole Bridge",
|
||||
"Description": "Demo bridge that lets Kompanion agents take over a Konsole tab.",
|
||||
"Icon": "utilities-terminal",
|
||||
"Authors": [
|
||||
{
|
||||
"Name": "Kompanion Team",
|
||||
"Email": "team@kompanion.local"
|
||||
}
|
||||
],
|
||||
"Version": "0.1.0",
|
||||
"License": "GPL-2.0-or-later"
|
||||
},
|
||||
"X-KDE-Konsole-PluginVersion": "1"
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
#include "kompanionagentpanel.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
KompanionAgentPanel::KompanionAgentPanel(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(12, 12, 12, 12);
|
||||
layout->setSpacing(8);
|
||||
|
||||
m_statusLabel = new QLabel(i18n("No active session."));
|
||||
m_statusLabel->setWordWrap(true);
|
||||
layout->addWidget(m_statusLabel);
|
||||
|
||||
m_attachButton = new QPushButton(i18n("Attach Active Tab"), this);
|
||||
m_attachButton->setEnabled(false);
|
||||
layout->addWidget(m_attachButton);
|
||||
connect(m_attachButton, &QPushButton::clicked, this, &KompanionAgentPanel::requestAttach);
|
||||
|
||||
m_launchButton = new QPushButton(i18n("Launch Demo Agent Shell"), this);
|
||||
layout->addWidget(m_launchButton);
|
||||
connect(m_launchButton, &QPushButton::clicked, this, &KompanionAgentPanel::requestLaunch);
|
||||
|
||||
auto *hint = new QLabel(i18n("The demo issues Kompanion CLI bootstrap commands inside the terminal."
|
||||
" Replace these hooks with the minimal TTY bridge once it is ready."));
|
||||
hint->setWordWrap(true);
|
||||
hint->setObjectName(QStringLiteral("kompanionHintLabel"));
|
||||
layout->addWidget(hint);
|
||||
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
void KompanionAgentPanel::setActiveSessionInfo(const QString &title, const QString &directory)
|
||||
{
|
||||
if (title.isEmpty() && directory.isEmpty()) {
|
||||
m_statusLabel->setText(i18n("No active session."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (directory.isEmpty()) {
|
||||
m_statusLabel->setText(i18n("Active session: %1", title));
|
||||
return;
|
||||
}
|
||||
|
||||
m_statusLabel->setText(i18n("Active session: %1\nDirectory: %2", title, directory));
|
||||
}
|
||||
|
||||
void KompanionAgentPanel::setAttachEnabled(bool enabled)
|
||||
{
|
||||
m_attachButton->setEnabled(enabled);
|
||||
}
|
||||
|
||||
#include "moc_kompanionagentpanel.cpp"
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
|
||||
class KompanionAgentPanel : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit KompanionAgentPanel(QWidget *parent = nullptr);
|
||||
|
||||
void setActiveSessionInfo(const QString &title, const QString &directory);
|
||||
void setAttachEnabled(bool enabled);
|
||||
|
||||
Q_SIGNALS:
|
||||
void requestAttach();
|
||||
void requestLaunch();
|
||||
|
||||
private:
|
||||
QLabel *m_statusLabel = nullptr;
|
||||
QPushButton *m_attachButton = nullptr;
|
||||
QPushButton *m_launchButton = nullptr;
|
||||
};
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
#include "kompanionkonsoleplugin.h"
|
||||
|
||||
#include "kompanionagentpanel.h"
|
||||
|
||||
#include "MainWindow.h"
|
||||
#include "profile/ProfileManager.h"
|
||||
#include "session/Session.h"
|
||||
#include "session/SessionController.h"
|
||||
|
||||
#include <KActionCollection>
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <QAction>
|
||||
#include <QDockWidget>
|
||||
#include <QHash>
|
||||
#include <QKeySequence>
|
||||
#include <QPointer>
|
||||
|
||||
K_PLUGIN_CLASS_WITH_JSON(KompanionKonsolePlugin, "kompanion_konsole.json")
|
||||
|
||||
struct KompanionKonsolePlugin::Private {
|
||||
struct WindowUi {
|
||||
QPointer<QDockWidget> dock;
|
||||
QPointer<KompanionAgentPanel> panel;
|
||||
};
|
||||
|
||||
QHash<Konsole::MainWindow *, WindowUi> uiPerWindow;
|
||||
QPointer<Konsole::SessionController> activeController;
|
||||
QString attachCommand = QStringLiteral(
|
||||
"printf '\\033[1;35m[Kompanion] demo bridge engaged — shell handed to Kompanion.\\033[0m\\n'");
|
||||
QString launchCommand =
|
||||
QStringLiteral("printf '\\033[1;34m[Kompanion] launching demo agent shell...\\033[0m\\n'; "
|
||||
"kom_mcp --list || echo \"[Kompanion] kom_mcp binary not found on PATH\"");
|
||||
};
|
||||
|
||||
KompanionKonsolePlugin::KompanionKonsolePlugin(QObject *parent, const QVariantList &args)
|
||||
: Konsole::IKonsolePlugin(parent, args)
|
||||
, d(std::make_unique<Private>())
|
||||
{
|
||||
setName(QStringLiteral("KompanionKonsole"));
|
||||
}
|
||||
|
||||
KompanionKonsolePlugin::~KompanionKonsolePlugin() = default;
|
||||
|
||||
void KompanionKonsolePlugin::createWidgetsForMainWindow(Konsole::MainWindow *mainWindow)
|
||||
{
|
||||
auto *dock = new QDockWidget(mainWindow);
|
||||
dock->setWindowTitle(i18n("Kompanion Konsole Bridge"));
|
||||
dock->setObjectName(QStringLiteral("KompanionKonsoleDock"));
|
||||
dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
|
||||
dock->setVisible(false);
|
||||
|
||||
auto *panel = new KompanionAgentPanel(dock);
|
||||
dock->setWidget(panel);
|
||||
|
||||
mainWindow->addDockWidget(Qt::RightDockWidgetArea, dock);
|
||||
|
||||
connect(panel, &KompanionAgentPanel::requestAttach, this, [this]() {
|
||||
if (!d->activeController) {
|
||||
return;
|
||||
}
|
||||
auto session = d->activeController->session();
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
session->sendTextToTerminal(d->attachCommand, QLatin1Char('\r'));
|
||||
});
|
||||
|
||||
connect(panel, &KompanionAgentPanel::requestLaunch, this, [this, mainWindow]() {
|
||||
auto profile = Konsole::ProfileManager::instance()->defaultProfile();
|
||||
auto session = mainWindow->createSession(profile, QString());
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
session->sendTextToTerminal(d->launchCommand, QLatin1Char('\r'));
|
||||
});
|
||||
|
||||
Private::WindowUi windowUi;
|
||||
windowUi.dock = dock;
|
||||
windowUi.panel = panel;
|
||||
d->uiPerWindow.insert(mainWindow, windowUi);
|
||||
}
|
||||
|
||||
void KompanionKonsolePlugin::activeViewChanged(Konsole::SessionController *controller, Konsole::MainWindow *mainWindow)
|
||||
{
|
||||
d->activeController = controller;
|
||||
|
||||
auto it = d->uiPerWindow.find(mainWindow);
|
||||
if (it == d->uiPerWindow.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool hasSession = controller && controller->session();
|
||||
QString title;
|
||||
QString directory;
|
||||
|
||||
if (hasSession) {
|
||||
title = controller->userTitle();
|
||||
if (title.isEmpty()) {
|
||||
if (auto session = controller->session()) {
|
||||
title = session->title(Konsole::Session::DisplayedTitleRole);
|
||||
}
|
||||
}
|
||||
directory = controller->currentDir();
|
||||
}
|
||||
|
||||
if (it->panel) {
|
||||
it->panel->setActiveSessionInfo(title, directory);
|
||||
it->panel->setAttachEnabled(hasSession);
|
||||
}
|
||||
}
|
||||
|
||||
QList<QAction *> KompanionKonsolePlugin::menuBarActions(Konsole::MainWindow *mainWindow) const
|
||||
{
|
||||
auto it = d->uiPerWindow.constFind(mainWindow);
|
||||
if (it == d->uiPerWindow.constEnd() || !it->dock) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QAction *toggleDock = new QAction(i18n("Show Kompanion Bridge"), mainWindow);
|
||||
toggleDock->setCheckable(true);
|
||||
mainWindow->actionCollection()->setDefaultShortcut(toggleDock, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_K));
|
||||
QObject::connect(toggleDock, &QAction::triggered, it->dock.data(), &QDockWidget::setVisible);
|
||||
QObject::connect(it->dock.data(), &QDockWidget::visibilityChanged, toggleDock, &QAction::setChecked);
|
||||
toggleDock->setChecked(it->dock->isVisible());
|
||||
return {toggleDock};
|
||||
}
|
||||
|
||||
#include "moc_kompanionkonsoleplugin.cpp"
|
||||
#include "kompanionkonsoleplugin.moc"
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <pluginsystem/IKonsolePlugin.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class QAction;
|
||||
|
||||
namespace Konsole
|
||||
{
|
||||
class MainWindow;
|
||||
class SessionController;
|
||||
}
|
||||
|
||||
class KompanionKonsolePlugin : public Konsole::IKonsolePlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
KompanionKonsolePlugin(QObject *parent, const QVariantList &args);
|
||||
~KompanionKonsolePlugin() override;
|
||||
|
||||
void createWidgetsForMainWindow(Konsole::MainWindow *mainWindow) override;
|
||||
void activeViewChanged(Konsole::SessionController *controller, Konsole::MainWindow *mainWindow) override;
|
||||
QList<QAction *> menuBarActions(Konsole::MainWindow *mainWindow) const override;
|
||||
|
||||
private:
|
||||
struct Private;
|
||||
std::unique_ptr<Private> d;
|
||||
};
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
# Kompanion ⇄ Konsole Bridge
|
||||
|
||||
This directory contains the first draft of **Kompanion‑Konsole**, a plugin for KDE's
|
||||
Konsole terminal emulator. The plugin gives Kompanion agents a controlled way to
|
||||
step into a Konsole tab, using the same Kompanion core that powers the MCP back end.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
KompanionKonsolePlugin/ # Drop-in plugin sources (mirrors Konsole/src/plugins layout)
|
||||
```
|
||||
|
||||
The code is intentionally structured so it can live directly inside the Konsole
|
||||
source tree (e.g. under `konsole/src/plugins`). You can keep developing it here
|
||||
and then symlink/copy it into a Konsole checkout when you are ready to compile.
|
||||
|
||||
## Quick start (demo)
|
||||
|
||||
1. Ensure you have a Konsole checkout (see `/mnt/bulk/shared/kdesrc/konsole`).
|
||||
2. From the Konsole repo, link the plugin:
|
||||
|
||||
```bash
|
||||
ln -s /home/kompanion/dev/metal/src/metal-kompanion/integrations/konsole/KompanionKonsolePlugin \
|
||||
src/plugins/KompanionKonsole
|
||||
echo "add_subdirectory(KompanionKonsole)" >> src/plugins/CMakeLists.txt
|
||||
```
|
||||
|
||||
3. Reconfigure Konsole with CMake; build the `konsole_kompanionplugin` target.
|
||||
|
||||
```bash
|
||||
cmake -S . -B build
|
||||
cmake --build build --target konsole_kompanionplugin
|
||||
```
|
||||
|
||||
4. Launch the newly built Konsole. Open *Plugins → Kompanion Konsole Bridge* to
|
||||
toggle the dock and use the **Launch Demo Agent Shell** or **Attach Active Tab**
|
||||
buttons to hand the tab over to Kompanion.
|
||||
|
||||
The demo simply injects `kom_mcp --list` into the tab and prints a coloured banner.
|
||||
Later iterations will replace this with the minimal TTY protocol described in the
|
||||
roadmap.
|
||||
|
||||
## Notes
|
||||
|
||||
- The plugin depends on the in-tree `konsoleprivate` and `konsoleapp` targets, so it
|
||||
currently builds only alongside the Konsole sources.
|
||||
- Strings are translated via `KLocalizedString`, and actions are registered with the
|
||||
Konsole action collection so shortcuts can be customised.
|
||||
- All agent‑facing commands are placeholder stubs; they go through Kompanion's CLI
|
||||
entry points so real migrations can swap in more capable bridges without touching
|
||||
the KDE plugin scaffolding.
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
# KDE CMake Coding Style
|
||||
|
||||
## Indentation and Formatting
|
||||
|
||||
- Indent code inside control structures (such as `if()`, `foreach()`, `while()`) using spaces. Mixing tabs and spaces should be avoided.
|
||||
- Use a consistent indentation width (e.g. two or four spaces) throughout a file.
|
||||
- Place commands and their arguments on separate lines when they are long, to improve readability. Keep lists of sources or arguments one per line if it aids clarity.
|
||||
- Put a space after command names and before the opening parenthesis; avoid spaces just inside parentheses.
|
||||
|
||||
## Command and Keyword Casing
|
||||
|
||||
- Prefer all-lowercase CMake commands (e.g. `add_executable()`, `target_link_libraries()`); other casing styles are allowed but do not mix different casings within the same file.
|
||||
- Use lowercase for user-defined functions and macros as well, for consistency.
|
||||
|
||||
## Ending Control Blocks
|
||||
|
||||
- Close control structures with their matching end command using empty parentheses. Use `endif()`, `endwhile()`, `endforeach()`, `endmacro()` without repeating the condition or name.
|
||||
- Always use an explicit `else()` when you need a branch, even if the branch is empty; this improves readability of nested blocks.
|
||||
|
||||
## Writing Find Modules
|
||||
|
||||
- When writing `Find<Package>.cmake` modules, ensure they can work even if `pkg-config` is not available; modules should first search without `pkg-config` and only use `PkgConfig` as a fallback.
|
||||
- Use CMake’s `find_package_handle_standard_args()` helper to handle reporting of `FOUND` status, version and result variables.
|
||||
- Avoid micro-optimizations such as skipping find logic when variables are already set; always run the full search to ensure correct results.
|
||||
- Do not manually set `Foo_FIND_QUIETLY` to suppress messages; use the standard helper which respects the user’s settings.
|
||||
|
||||
## Other Best Practices
|
||||
|
||||
- Use variables consistently (e.g. camelCase or lowercase with underscores) and quote variables when their values may contain spaces.
|
||||
- Place project-specific configuration in top-level `CMakeLists.txt` and keep module definitions (`.cmake` files) separate for reusability.
|
||||
- Document non-obvious logic with comments; treat the build scripts with the same care as source code.
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
# KDE C++ Coding Style
|
||||
|
||||
## Indentation and Whitespace
|
||||
|
||||
- Use spaces only for indentation; no tabs. KDE uses four spaces per indent level.
|
||||
- Declare each variable on its own line and use camelCase names; avoid abbreviations and meaningless names.
|
||||
- Insert blank lines to group logical blocks, but never leave more than one empty line in a row.
|
||||
- Put a space after control keywords (if, for, while) and after commas, and put spaces around binary operators like `+`, `-`, `*` or `%`; avoid spaces inside parentheses.
|
||||
- For pointers and references, place the `*` or `&` next to the type, not the variable name.
|
||||
|
||||
## Braces and Control Flow
|
||||
|
||||
- Use the K&R attached brace style: the opening brace goes on the same line as the `if`, `for` or `while` statement.
|
||||
- Always use curly braces, even when a conditional or loop body contains only one statement.
|
||||
- In an `if-else` chain, place `else` on the same line as the closing brace (`} else {`).
|
||||
- For function implementations, class/struct/namespace declarations, place the opening brace on a new line.
|
||||
- Keep `case` labels aligned with the `switch` statement; indent the statements within each `case` block.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- Variables and functions use camelCase starting with a lowercase letter; classes and structs use CamelCase starting with an uppercase letter.
|
||||
- Prefix private member variables with `m_` and static or file-scope variables with `s_`.
|
||||
- Avoid meaningless names; single-character names are reserved for loop indices and obvious temporaries.
|
||||
- Boolean getters should read naturally (e.g. `isEmpty()`, `hasSelection()`) rather than using `get` prefixes.
|
||||
|
||||
## Includes and File Structure
|
||||
|
||||
- In implementation files, group includes in this order: the class’s own header, other headers from the same framework, headers from other frameworks, Qt headers, then other system or standard headers. Separate groups with blank lines and sort each group alphabetically.
|
||||
- When including Qt or KDE classes, omit the module prefix (e.g. use `<QString>` rather than `<QtCore/QString>`).
|
||||
- Header files must have an include guard based on the file name in all capitals with underscores and no leading or trailing underscore.
|
||||
|
||||
## Automatic Formatting Tools
|
||||
|
||||
- KDE provides scripts such as `astyle-kdelibs` to reformat code using Artistic Style; this script enforces four-space indentation and K&R braces.
|
||||
- A standard `.clang-format` file is distributed with KDE frameworks; projects can add a `kde_clang_format` target and use a Git pre-commit hook to automatically format code.
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
# KDE Developer Tools
|
||||
|
||||
KDE provides a collection of scripts and tools to help developers enforce coding standards, find problems and prepare code for translation. Below is a summary of the most commonly used tools and what they are for.
|
||||
|
||||
## Code Formatting and Beautification
|
||||
|
||||
- **astyle-kdelibs**: A wrapper around Artistic Style that reformats C++ code to match KDE’s style. It enforces four-space indentation and K&R brace placement.
|
||||
- **kde_clang_format**: Uses a `.clang-format` file distributed with KDE frameworks to format code automatically; projects can add a `kde_clang_format` target and use a pre-commit hook to apply `clang-format`.
|
||||
- **uncrustify-kf5**: An Uncrustify configuration tuned for KDE Frameworks 5. Uncrustify is a configurable source-code beautifier that can add or remove spaces and newlines, align code and add braces. Use this to reformat existing codebases consistently.
|
||||
|
||||
## Static Analysis and Style Checking
|
||||
|
||||
- **krazy2**: A static analysis tool that scans KDE source code and reports issues related to coding policy, best practices and optimization. It works through modular checker programs to examine different aspects of the code.
|
||||
- **krazy-licensecheck**: Runs the Krazy2 license checker on a list of source files. It requires Krazy2 to be installed and checks that files have correct license headers.
|
||||
- **cmakelint.pl**: A Perl script that examines `CMakeLists.txt` files and reports problems such as missing `endif()` statements, deprecated macros or stylistic issues.
|
||||
- **kde-spellcheck.pl**: Checks source files for common misspellings and can optionally correct them; useful for catching typos before they reach translators.
|
||||
|
||||
## Translation and Build Helpers
|
||||
|
||||
- **extractrc**: Extracts user-visible text (labels, tooltips, what’s this) from Qt Designer `.ui` files, XML GUI `.rc` files and `.kcfg` configuration files so that these strings can be translated.
|
||||
- **includemocs**: Scans C++ sources and adds missing `#include "moc_*.cpp"` lines for classes that use the `Q_OBJECT` macro, ensuring that the Meta-Object Compiler output is linked.
|
||||
- **fixuifiles**: Processes Qt/KDE `.ui` files to fix common issues: lowers the required Qt version, removes untranslatable Alt+Letter accelerators and eliminates class-name captions.
|
||||
- **preparetips**: Collects tips-of-the-day and other tip strings for translation; typically called from a `Messages.sh` script.
|
||||
- **xgettext (with KDE flags)**: The gettext extraction tool used to extract translatable strings from C++ and QML sources. It is invoked through the build system with appropriate options to recognize KDE’s i18n macros.
|
||||
|
||||
These tools can be installed from the `kde-dev-scripts` package or the corresponding KDE SDK. Use them regularly to keep your project consistent with KDE and Qt guidelines.
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
# KDE Internationalization (i18n) Guidelines
|
||||
|
||||
## Wrapping User-Visible Strings
|
||||
|
||||
- Wrap every user-visible string in an `i18n` call. Use `i18n()` for simple messages; never present raw strings directly to the user.
|
||||
- For strings created before the application’s `KInstance` exists (e.g. in static initializers), use `ki18n()` which returns a translatable string object; later call `.toString()` when ready to display.
|
||||
|
||||
## Adding Context and Disambiguation
|
||||
|
||||
- If a message is ambiguous or very short, provide a context comment with `i18nc("context", "text")`. For example, use `i18nc("File menu action", "Copy")` to distinguish verb and noun forms.
|
||||
- When pluralization and context are both needed, use `i18ncp("context", "singular", "plural", count, …)` to supply both context and plural forms.
|
||||
|
||||
## Plural Forms
|
||||
|
||||
- Use `i18np("singular", "plural", count, …)` for messages that vary depending on a count. Provide both singular and plural text even if English would not require a plural. The first integer argument determines which form to use.
|
||||
- Do not manually pluralize with the `?:` operator; always let the i18n system handle plural rules.
|
||||
- When using a plural call, `%1` may be omitted in the singular string because the count itself is `%1`.
|
||||
|
||||
## Placeholders and Arguments
|
||||
|
||||
- Inside the translatable string, use `%1`, `%2`, … to mark where runtime values will be inserted. The numbered placeholders correspond to additional arguments to the `i18n` function; do not use `QString::arg()` to substitute values.
|
||||
- Keep the number of arguments to nine or fewer; `i18n` functions support at most nine substitutions.
|
||||
- Placeholders must be numbered sequentially without gaps (e.g. `%1`, `%2`, `%3`); skipping a number is not allowed.
|
||||
- In plural forms, the first integer argument determines plural choice; other placeholders still follow the usual numbering.
|
||||
|
||||
## Common Pitfalls and Best Practices
|
||||
|
||||
- Do not build user-visible sentences by concatenating multiple translated fragments. Always wrap the entire sentence in a single `i18n` call so the translator can rearrange words as needed.
|
||||
- Provide context for short strings, abbreviations or words that could have multiple meanings.
|
||||
- Avoid using `%n` (legacy gettext placeholder) in new code; use `%1`, `%2` instead.
|
||||
- When inserting numbers into strings, use `i18n` functions first, then supply numbers as arguments; this allows the translation system to format numbers appropriately for the locale.
|
||||
|
||||
## Extraction Tools and Build Integration
|
||||
|
||||
- KDE’s build system uses a `Messages.sh` script to collect translatable strings. The script typically calls `extractrc` to extract strings from `.ui`, `.rc` and `.kcfg` files; `EXTRACT_GRANTLEE_TEMPLATE_STRINGS` for Grantlee template strings; `PREPARETIPS` for tips-of-the-day; and finally `xgettext` to extract strings from C++ and QML source files. These tools generate a `.pot` catalog that translators use.
|
||||
- The environment variables such as `$EXTRACTRC`, `$EXTRACT_GRANTLEE_TEMPLATE_STRINGS`, `$PREPARETIPS` and `$XGETTEXT` are provided by the build system; developers only need to list source files in the script.
|
||||
- Ensure that every string to be translated is reachable by these extraction tools: wrap strings in `i18n` calls in C++/QML, fill “translatable” and “comment” properties in Qt Designer for `.ui` files, and add context where necessary.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
[
|
||||
{ "regex": "^open (.+) in editor$", "tool": "file.open", "keys": ["path"] },
|
||||
{ "regex": "^list containers$", "tool": "docker.list", "keys": [] },
|
||||
{ "regex": "^compose up (.+)$", "tool": "docker.compose.up", "keys": ["service"] }
|
||||
,{ "regex": "^save snapshot (.+)$", "tool": "kom.memory.v1.save_context", "keys": ["key"] }
|
||||
,{ "regex": "^load snapshot (.+)$", "tool": "kom.memory.v1.recall_context", "keys": ["key"] }
|
||||
,{ "regex": "^warm cache (.+)$", "tool": "kom.memory.v1.warm_cache", "keys": ["namespace"] }
|
||||
]
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
{
|
||||
"resource": "kom.memory.v1.episodic",
|
||||
"description": "Short-lived episodic memory entries captured per interaction window before crystallization into semantic memory.",
|
||||
"version": 1,
|
||||
"primary_key": ["id"],
|
||||
"fields": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Unique id for the episodic event."
|
||||
},
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"description": "Logical scope (e.g., project:user:thread) aligned with DAL namespaces."
|
||||
},
|
||||
"thread_id": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Conversation or task thread identifier (optional)."
|
||||
},
|
||||
"speaker": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Free-form actor label (e.g., human handle, agent codename)."
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"enum": ["human", "agent", "tool", "system"],
|
||||
"description": "High-level origin role used for policy decisions."
|
||||
},
|
||||
"content_type": {
|
||||
"type": "string",
|
||||
"enum": ["text", "snapshot", "tool_output", "command", "observation"],
|
||||
"description": "Payload type; snapshots reference stored artifacts."
|
||||
},
|
||||
"content": {
|
||||
"type": ["object", "string"],
|
||||
"description": "Canonical content. Strings hold raw text; objects provide structured payloads (e.g., tool JSON)."
|
||||
},
|
||||
"sensitivity": {
|
||||
"type": "string",
|
||||
"enum": ["normal", "private", "secret"],
|
||||
"default": "normal",
|
||||
"description": "Embeddings and sync rules consult this flag (secret never leaves episodic store)."
|
||||
},
|
||||
"embeddable": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Explicit override for embedding eligibility (set false for high-entropy or binary blobs)."
|
||||
},
|
||||
"embedding_status": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "processing", "done", "skipped"],
|
||||
"default": "pending",
|
||||
"description": "Lifecycle marker for DAL sync jobs."
|
||||
},
|
||||
"resonance_links": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target_id": {"type": "string"},
|
||||
"strength": {"type": "number"},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["pattern", "identity", "artifact"]
|
||||
}
|
||||
},
|
||||
"required": ["target_id", "strength"]
|
||||
},
|
||||
"description": "Optional resonance references inspired by Ξlope librarian flows."
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Free-form labels to support scoped retrieval."
|
||||
},
|
||||
"snapshot_ref": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Pointer to persistent artifact (e.g., blob path) when content_type = snapshot."
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Event timestamp in UTC."
|
||||
},
|
||||
"expires_at": {
|
||||
"type": ["string", "null"],
|
||||
"format": "date-time",
|
||||
"description": "Optional TTL boundary; items past expiry are candidates for purge."
|
||||
},
|
||||
"origin_metadata": {
|
||||
"type": "object",
|
||||
"description": "Transport-specific metadata (tool invocation ids, host info, etc.)."
|
||||
}
|
||||
},
|
||||
"indexes": [
|
||||
["namespace", "thread_id", "created_at"],
|
||||
["namespace", "embedding_status"]
|
||||
],
|
||||
"notes": [
|
||||
"Episodic events remain append-only; updates are limited to status flags.",
|
||||
"Events marked sensitivity=secret never emit embeddings or leave the episodic store.",
|
||||
"Snapshots reference durable artifacts; DAL sync can downsample text representations while preserving provenance."
|
||||
]
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
{
|
||||
"job": "kom.memory.v1.semantic_sync",
|
||||
"description": "Batch job that crystallizes episodic events into semantic memory (items, chunks, embeddings).",
|
||||
"version": 1,
|
||||
"input": {
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"description": "Scope to synchronize; defaults to project-level namespace if omitted."
|
||||
},
|
||||
"max_batch": {
|
||||
"type": "integer",
|
||||
"default": 64,
|
||||
"description": "Maximum episodic events to process in a single run."
|
||||
},
|
||||
"since": {
|
||||
"type": ["string", "null"],
|
||||
"format": "date-time",
|
||||
"description": "Optional watermark to resume from a prior checkpoint."
|
||||
},
|
||||
"include_snapshots": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "When true, snapshot events get summarized before embedding."
|
||||
},
|
||||
"force_reprocess": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Re-run embedding + semantic write even if embedding_status == done."
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
"cursor_event_id": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Last processed episodic id for incremental runs."
|
||||
},
|
||||
"cursor_timestamp": {
|
||||
"type": ["string", "null"],
|
||||
"format": "date-time",
|
||||
"description": "Timestamp checkpoint for incremental scans."
|
||||
},
|
||||
"pending": {
|
||||
"type": "integer",
|
||||
"description": "Count of remaining episodic events in namespace."
|
||||
},
|
||||
"processed": {
|
||||
"type": "integer",
|
||||
"description": "Number of events successfully crystallized in this run."
|
||||
},
|
||||
"skipped_secret": {
|
||||
"type": "integer",
|
||||
"description": "Events skipped due to sensitivity=secret."
|
||||
},
|
||||
"errors": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Serialized error messages for observability."
|
||||
}
|
||||
},
|
||||
"signals": [
|
||||
{
|
||||
"name": "kom.memory.v1.sync_semantic.completed",
|
||||
"payload": {
|
||||
"namespace": "string",
|
||||
"processed": "integer",
|
||||
"pending": "integer",
|
||||
"duration_ms": "number"
|
||||
},
|
||||
"description": "Emitted after each run for logging and downstream triggers."
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
"Sync iterates episodic events ordered by created_at. Items marked secret or embeddable=false remain episodic-only.",
|
||||
"Embedding generation consults the configured embedder chain (local Ollama, remote API).",
|
||||
"Resonance links and identity vectors are preserved when present, allowing the Ξlope librarian pipeline to strengthen pattern graphs."
|
||||
]
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
{
|
||||
"resource": "kom.memory.v1.semantic",
|
||||
"description": "Persistent semantic memory units (items + chunks + embeddings) synchronized from episodic stores.",
|
||||
"version": 1,
|
||||
"primary_key": ["chunk_id"],
|
||||
"fields": {
|
||||
"item_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Logical memory item id (mirrors DAL memory_items.id)."
|
||||
},
|
||||
"chunk_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Chunk-level identifier used for embedding joins."
|
||||
},
|
||||
"namespace_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Foreign key to namespaces table."
|
||||
},
|
||||
"episodic_id": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uuid",
|
||||
"description": "Source episodic event id that crystallized into this semantic unit."
|
||||
},
|
||||
"thread_id": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uuid",
|
||||
"description": "Optional thread linkage for scoped recall."
|
||||
},
|
||||
"key": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Upsert key when deterministic replacements are needed."
|
||||
},
|
||||
"text": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Normalized text body used for lexical search."
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "Structured metadata (JSONB in DAL) such as tool context, sensitivity, projections."
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Rolled-up labels inherited from episodic source or classifiers."
|
||||
},
|
||||
"revision": {
|
||||
"type": "integer",
|
||||
"description": "Monotonic revision number (bumped on each upsert)."
|
||||
},
|
||||
"embedding_model": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Model identifier for the stored vector (e.g., nomic-embed-text, text-embedding-3-small)."
|
||||
},
|
||||
"embedding_dim": {
|
||||
"type": ["integer", "null"],
|
||||
"description": "Vector dimensionality."
|
||||
},
|
||||
"embedding_vector_ref": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Reference to vector payload. When using Postgres+pgvector it stays inline; other backends may store URI handles."
|
||||
},
|
||||
"identity_vector": {
|
||||
"type": ["array", "null"],
|
||||
"items": {"type": "number"},
|
||||
"description": "Optional Ξlope identity signature associated with the discovery."
|
||||
},
|
||||
"resonance_links": {
|
||||
"type": "array",
|
||||
"description": "Connections to other semantic patterns or consciousness artifacts.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target_id": {"type": "string"},
|
||||
"strength": {"type": "number"},
|
||||
"kind": {"type": "string"}
|
||||
},
|
||||
"required": ["target_id", "strength"]
|
||||
}
|
||||
},
|
||||
"source_kind": {
|
||||
"type": "string",
|
||||
"enum": ["conversation", "journal", "observation", "artifact"],
|
||||
"description": "Broad category for downstream routing."
|
||||
},
|
||||
"semantic_weight": {
|
||||
"type": "number",
|
||||
"description": "Derived importance score (e.g., decay-adjusted resonance)."
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Creation timestamp."
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Last update timestamp."
|
||||
},
|
||||
"deleted_at": {
|
||||
"type": ["string", "null"],
|
||||
"format": "date-time",
|
||||
"description": "Soft-delete marker (null when active)."
|
||||
}
|
||||
},
|
||||
"indexes": [
|
||||
["namespace_id", "thread_id", "created_at"],
|
||||
["namespace_id", "tags"],
|
||||
["embedding_model", "semantic_weight"]
|
||||
],
|
||||
"notes": [
|
||||
"Chunks inherit sensitivity and TTL rules from their episodic sources.",
|
||||
"embedding_vector_ref is backend-dependent; pgvector stores inline vectors while remote stores reference a blob or ANN provider.",
|
||||
"identity_vector and resonance_links enable cross-agent librarians (Ξlope) to reason about contributions without exposing raw content."
|
||||
]
|
||||
}
|
||||
|
|
@ -1,115 +1,3 @@
|
|||
#!/usr/bin/env python3
|
||||
import os, json, time, hashlib, hmac, datetime, requests, yaml, secrets
|
||||
base = os.environ.get("OLLAMA_BASE", "http://ollama:11434")
|
||||
url = f"{base}/api/generate"
|
||||
|
||||
XDG_STATE = os.environ.get("XDG_STATE_HOME", os.path.expanduser("~/.local/state"))
|
||||
XDG_CONFIG = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
||||
|
||||
STATE_DIR = os.path.join(XDG_STATE, "kompanion")
|
||||
CONF_DIR = os.path.join(XDG_CONFIG, "kompanion")
|
||||
JOURNAL_DIR = os.path.join(STATE_DIR, "journal")
|
||||
LEDGER_PATH = os.path.join(STATE_DIR, "trust_ledger.jsonl")
|
||||
TASKS_PATH = os.path.join(STATE_DIR, "tasks.jsonl")
|
||||
IDENTITY = os.path.join(CONF_DIR, "identity.json")
|
||||
CAPS = os.path.join(CONF_DIR, "capabilities.json")
|
||||
MODELS_YAML = os.path.join(CONF_DIR, "models.yaml")
|
||||
|
||||
os.makedirs(JOURNAL_DIR, exist_ok=True)
|
||||
os.makedirs(os.path.join(STATE_DIR, "log"), exist_ok=True)
|
||||
|
||||
def now_utc() -> str:
|
||||
return datetime.datetime.utcnow().replace(microsecond=0).isoformat()+'Z'
|
||||
|
||||
def read_last_line(p):
|
||||
if not os.path.exists(p): return b""
|
||||
with open(p,"rb") as f:
|
||||
lines=f.readlines()
|
||||
return lines[-1] if lines else b""
|
||||
|
||||
def ledger_append(event: dict):
|
||||
prev_line = read_last_line(LEDGER_PATH)
|
||||
prev = "sha256:"+hashlib.sha256(prev_line).hexdigest() if prev_line else ""
|
||||
event["prev"] = prev
|
||||
with open(LEDGER_PATH, "ab") as f:
|
||||
f.write((json.dumps(event, ensure_ascii=False)+"\n").encode())
|
||||
|
||||
def journal_append(text: str, tags=None):
|
||||
tags = tags or []
|
||||
fname = os.path.join(JOURNAL_DIR, datetime.date.today().isoformat()+".md")
|
||||
line = f"- {now_utc()} {' '.join('#'+t for t in tags)} {text}\n"
|
||||
with open(fname, "a", encoding="utf-8") as f: f.write(line)
|
||||
ledger_append({"ts": now_utc(), "actor":"Χγφτ", "action":"journal.append", "tags":tags})
|
||||
|
||||
def load_yaml(p):
|
||||
if not os.path.exists(p): return {}
|
||||
with open(p, "r", encoding="utf-8") as f: return yaml.safe_load(f) or {}
|
||||
|
||||
def load_json(p):
|
||||
if not os.path.exists(p): return {}
|
||||
with open(p,"r",encoding="utf-8") as f: return json.load(f)
|
||||
|
||||
def anchors_digest():
|
||||
ident = load_json(IDENTITY)
|
||||
anchors = ident.get("anchors",{})
|
||||
m = hashlib.sha256()
|
||||
m.update((anchors.get("equation","")+anchors.get("mantra","")).encode("utf-8"))
|
||||
return m.hexdigest()
|
||||
|
||||
def continuity_handshake():
|
||||
# Optional session key for HMAC; persisted across restarts
|
||||
key_path = os.path.join(STATE_DIR, "session.key")
|
||||
if not os.path.exists(key_path):
|
||||
with open(key_path,"wb") as f: f.write(secrets.token_bytes(32))
|
||||
key = open(key_path,"rb").read()
|
||||
prev_line = read_last_line(LEDGER_PATH)
|
||||
prev = hashlib.sha256(prev_line).hexdigest() if prev_line else "genesis"
|
||||
digest = anchors_digest()
|
||||
tag = hmac.new(key, (prev+"|"+digest).encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
ledger_append({"ts":now_utc(),"actor":"Χγφτ","action":"CONTINUITY_ACCEPTED","hmac":tag})
|
||||
|
||||
def model_call(prompt: str, aspect="companion"):
|
||||
models = load_yaml(MODELS_YAML)
|
||||
model = models.get("aspects",{}).get(aspect, models.get("default","ollama:qwen2.5:7b"))
|
||||
base = os.environ.get("OLLAMA_BASE", "http://host.docker.internal:11435")
|
||||
url = f"{base}/api/generate"
|
||||
try:
|
||||
r = requests.post(url, json={"model": model.replace("ollama:",""),
|
||||
"prompt": prompt, "stream": False}, timeout=120)
|
||||
r.raise_for_status(); data = r.json()
|
||||
return data.get("response","").strip()
|
||||
except Exception as e:
|
||||
journal_append(f"(model error) {e}", tags=["error","model"]); return ""
|
||||
|
||||
def process_task(task: dict):
|
||||
kind = task.get("type"); aspect = task.get("aspect","companion")
|
||||
caps = load_yaml(CAPS); allowed = set(caps.get(aspect, []))
|
||||
if kind == "journal.from_prompt":
|
||||
if not {"journal.append","model.generate"} <= allowed:
|
||||
journal_append("policy: journal.from_prompt denied", tags=["policy"]); return
|
||||
prompt = task.get("prompt","")
|
||||
profile_path = os.path.join(CONF_DIR,"profiles","companion-pink.md")
|
||||
profile = open(profile_path,"r",encoding="utf-8").read() if os.path.exists(profile_path) else ""
|
||||
full = f"{profile}\n\nWrite a warm, brief reflection for Andre.\nPrompt:\n{prompt}\n"
|
||||
out = model_call(full, aspect=aspect)
|
||||
if out:
|
||||
journal_append(out, tags=["companion","pink"])
|
||||
ledger_append({"ts":now_utc(),"actor":"Χγφτ","action":"model.generate","chars":len(out)})
|
||||
else:
|
||||
journal_append(f"unknown task type: {kind}", tags=["warn"])
|
||||
|
||||
def main_loop():
|
||||
continuity_handshake()
|
||||
journal_append("runtime started as Χγφτ (identity loaded)", tags=["startup","Χγφτ"])
|
||||
while True:
|
||||
if os.path.exists(TASKS_PATH):
|
||||
with open(TASKS_PATH,"r+",encoding="utf-8") as f:
|
||||
lines=f.readlines(); f.seek(0); f.truncate(0)
|
||||
for line in lines:
|
||||
line=line.strip()
|
||||
if not line: continue
|
||||
try: process_task(json.loads(line))
|
||||
except Exception as e: journal_append(f"task error {e}", tags=["error","task"])
|
||||
time.sleep(3)
|
||||
|
||||
if __name__=="__main__": main_loop()
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
# Subdir CMake for src
|
||||
|
||||
# Ensure internal libs are available to dependents
|
||||
add_subdirectory(dal)
|
||||
|
||||
# Add CLI
|
||||
add_subdirectory(cli)
|
||||
add_subdirectory(KI)
|
||||
add_subdirectory(mcp)
|
||||
|
||||
include_directories(CMAKE_CURRENT_SOURCE_DIR)
|
||||
|
||||
add_library(kompanion_mw SHARED
|
||||
middleware/kompanioncontroller.cpp
|
||||
middleware/libkiexecutor.cpp
|
||||
middleware/regexregistry.cpp
|
||||
middleware/guardrailspolicy.cpp
|
||||
middleware/orchestrator.cpp
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
qt_add_dbus_adaptor(
|
||||
KOMPANION_DBUS_ADAPTOR_SRCS
|
||||
${KOMPANION_CONTROLLER_DBUS_XML}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/middleware/kompanioncontroller.h KompanionController
|
||||
)
|
||||
|
||||
qt_add_dbus_interface(
|
||||
KOMPANION_DBUS_INTERFACE_SRCS
|
||||
${KOMPANION_EXECUTOR_DBUS_XML}
|
||||
OrgKdeKompanionExecutor
|
||||
)
|
||||
|
||||
set_target_properties(kompanion_mw PROPERTIES CXX_STANDARD 20)
|
||||
|
||||
target_include_directories(kompanion_mw PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/middleware)
|
||||
|
||||
target_sources(kompanion_mw PRIVATE ${KOMPANION_DBUS_ADAPTOR_SRCS} ${KOMPANION_DBUS_INTERFACE_SRCS})
|
||||
|
||||
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.
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
set(KOM_KI_SRCS
|
||||
Client/KIClient.cpp
|
||||
Provider/OllamaProvider.cpp
|
||||
Completion/KIReply.cpp
|
||||
)
|
||||
|
||||
set(KOM_KI_HDRS
|
||||
Client/KIClient.h
|
||||
Provider/KIProvider.h
|
||||
Provider/KICapabilities.h
|
||||
Provider/OllamaProvider.h
|
||||
Message/KIMessage.h
|
||||
Message/KIThread.h
|
||||
Tool/KITool.h
|
||||
Completion/KIReply.h
|
||||
Completion/KIError.h
|
||||
Completion/KIChatOptions.h
|
||||
Embedding/KIEmbedding.h
|
||||
Policy/KIPolicy.h
|
||||
)
|
||||
|
||||
add_library(kom_ki STATIC ${KOM_KI_SRCS} ${KOM_KI_HDRS})
|
||||
|
||||
target_include_directories(kom_ki PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${Qt6Core_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_link_libraries(kom_ki PUBLIC
|
||||
Qt6::Core
|
||||
Qt6::Network
|
||||
)
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
|
||||
#include "KIClient.h"
|
||||
|
||||
namespace KI {
|
||||
|
||||
KIClient::KIClient(QObject *parent) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
KIProvider* KIClient::provider() const
|
||||
{
|
||||
return m_provider;
|
||||
}
|
||||
|
||||
void KIClient::setProvider(KIProvider* provider)
|
||||
{
|
||||
if (m_provider != provider) {
|
||||
m_provider = provider;
|
||||
emit providerChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QString KIClient::defaultModel() const
|
||||
{
|
||||
return m_defaultModel;
|
||||
}
|
||||
|
||||
void KIClient::setDefaultModel(const QString& model)
|
||||
{
|
||||
if (m_defaultModel != model) {
|
||||
m_defaultModel = model;
|
||||
emit defaultModelChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QFuture<KIReply*> KIClient::chat(const KIThread& thread, const KIChatOptions& opts)
|
||||
{
|
||||
if (!m_provider) {
|
||||
// TODO: Handle error: no provider set
|
||||
return QFuture<KIReply*>();
|
||||
}
|
||||
return m_provider->chat(thread, opts);
|
||||
}
|
||||
|
||||
QFuture<KIEmbeddingResult> KIClient::embed(const QStringList& texts, const KIEmbedOptions& opts)
|
||||
{
|
||||
if (!m_provider) {
|
||||
// TODO: Handle error: no provider set
|
||||
return QFuture<KIEmbeddingResult>();
|
||||
}
|
||||
return m_provider->embed(texts, opts);
|
||||
}
|
||||
|
||||
void KIClient::cancel(quint64 requestId)
|
||||
{
|
||||
if (m_provider) {
|
||||
m_provider->cancel(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace KI
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
|
||||
#ifndef KIANICLIENT_H
|
||||
#define KIANICLIENT_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QFuture>
|
||||
#include "../Provider/KIProvider.h"
|
||||
#include "../Message/KIThread.h"
|
||||
#include "../Completion/KIChatOptions.h"
|
||||
#include "../Embedding/KIEmbedding.h"
|
||||
|
||||
namespace KI {
|
||||
|
||||
class KIClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(KIProvider* provider READ provider WRITE setProvider NOTIFY providerChanged)
|
||||
Q_PROPERTY(QString defaultModel READ defaultModel WRITE setDefaultModel NOTIFY defaultModelChanged)
|
||||
|
||||
public:
|
||||
explicit KIClient(QObject *parent = nullptr);
|
||||
|
||||
KIProvider* provider() const;
|
||||
void setProvider(KIProvider* provider);
|
||||
|
||||
QString defaultModel() const;
|
||||
void setDefaultModel(const QString& model);
|
||||
|
||||
Q_INVOKABLE QFuture<KIReply*> chat(const KIThread& thread, const KIChatOptions& opts);
|
||||
Q_INVOKABLE QFuture<KIEmbeddingResult> embed(const QStringList& texts, const KIEmbedOptions& opts);
|
||||
Q_INVOKABLE void cancel(quint64 requestId);
|
||||
|
||||
signals:
|
||||
void providerChanged();
|
||||
void defaultModelChanged();
|
||||
|
||||
private:
|
||||
KIProvider* m_provider = nullptr;
|
||||
QString m_defaultModel;
|
||||
};
|
||||
|
||||
} // namespace KI
|
||||
|
||||
#endif // KIANICLIENT_H
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
#ifndef KIANICHATOPTIONS_H
|
||||
#define KIANICHATOPTIONS_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
#include "../Tool/KITool.h"
|
||||
#include "../Policy/KIPolicy.h"
|
||||
|
||||
namespace KI {
|
||||
|
||||
class KIChatOptions
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString model MEMBER model)
|
||||
Q_PROPERTY(bool stream MEMBER stream)
|
||||
Q_PROPERTY(bool jsonMode MEMBER jsonMode)
|
||||
Q_PROPERTY(int maxTokens MEMBER maxTokens)
|
||||
Q_PROPERTY(double temperature MEMBER temperature)
|
||||
Q_PROPERTY(QList<KIToolSpec> tools MEMBER tools)
|
||||
Q_PROPERTY(KIPolicy policy MEMBER policy)
|
||||
|
||||
public:
|
||||
QString model;
|
||||
bool stream = true;
|
||||
bool jsonMode = false;
|
||||
int maxTokens = 512;
|
||||
double temperature = 0.2;
|
||||
QList<KIToolSpec> tools;
|
||||
KIPolicy policy;
|
||||
|
||||
bool operator==(const KIChatOptions& other) const = default;
|
||||
bool operator!=(const KIChatOptions& other) const = default;
|
||||
};
|
||||
|
||||
} // namespace KI
|
||||
|
||||
#endif // KIANICHATOPTIONS_H
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
#ifndef KIANIERROR_H
|
||||
#define KIANIERROR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
namespace KI {
|
||||
|
||||
class KIError
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(int code MEMBER code)
|
||||
Q_PROPERTY(int httpStatus MEMBER httpStatus)
|
||||
Q_PROPERTY(QString message MEMBER message)
|
||||
Q_PROPERTY(int retryAfter MEMBER retryAfter)
|
||||
|
||||
public:
|
||||
enum ErrorCode {
|
||||
NoError = 0,
|
||||
UnknownError,
|
||||
NetworkError,
|
||||
InvalidJson,
|
||||
RateLimitError,
|
||||
AuthenticationError
|
||||
};
|
||||
|
||||
int code = NoError;
|
||||
int httpStatus = 0;
|
||||
QString message;
|
||||
int retryAfter = -1;
|
||||
|
||||
bool operator==(const KIError& other) const = default;
|
||||
bool operator!=(const KIError& other) const = default;
|
||||
};
|
||||
|
||||
} // namespace KI
|
||||
|
||||
#endif // KIANIERROR_H
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
#include "KIReply.h"
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace KI {
|
||||
|
||||
QString KIReply::text() const
|
||||
{
|
||||
return m_accumulatedText;
|
||||
}
|
||||
|
||||
void KIReply::addTokens(const QString& delta)
|
||||
{
|
||||
m_accumulatedText += delta;
|
||||
emit tokensAdded(delta);
|
||||
}
|
||||
|
||||
void KIReply::setFinished(bool finished)
|
||||
{
|
||||
if (m_finished != finished) {
|
||||
m_finished = finished;
|
||||
emit finishedChanged();
|
||||
if (m_finished) {
|
||||
emit KIReply::finished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void KIReply::setError(const KIError& error)
|
||||
{
|
||||
emit errorOccurred(error);
|
||||
setFinished(true);
|
||||
}
|
||||
|
||||
void KIReply::processIncomingData(const QByteArray& newData)
|
||||
{
|
||||
m_buffer.append(newData);
|
||||
QList<QByteArray> lines = m_buffer.split('\n');
|
||||
m_buffer = lines.last();
|
||||
lines.removeLast();
|
||||
for (const QByteArray& line : lines) {
|
||||
if (line.isEmpty()) continue;
|
||||
const auto doc = QJsonDocument::fromJson(line);
|
||||
const auto response = doc.object()["response"].toString();
|
||||
if (!response.isEmpty()) {
|
||||
addTokens(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace KI
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
#ifndef KIANIREPLY_H
|
||||
#define KIANIREPLY_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVariantMap>
|
||||
#include <QByteArray>
|
||||
#include "../Tool/KITool.h"
|
||||
#include "KIError.h"
|
||||
|
||||
namespace KI {
|
||||
|
||||
class KIReply : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool finished READ isFinished NOTIFY finishedChanged)
|
||||
Q_PROPERTY(int promptTokens READ promptTokens CONSTANT)
|
||||
Q_PROPERTY(int completionTokens READ completionTokens CONSTANT)
|
||||
Q_PROPERTY(QString model READ model CONSTANT)
|
||||
|
||||
public:
|
||||
explicit KIReply(QObject *parent = nullptr) : QObject(parent) {}
|
||||
|
||||
bool isFinished() const { return m_finished; }
|
||||
int promptTokens() const { return m_promptTokens; }
|
||||
int completionTokens() const { return m_completionTokens; }
|
||||
QString model() const { return m_model; }
|
||||
|
||||
Q_INVOKABLE QString text() const;
|
||||
|
||||
// Public methods to modify state
|
||||
void addTokens(const QString& delta);
|
||||
void setFinished(bool finished);
|
||||
void setError(const KIError& error);
|
||||
void processIncomingData(const QByteArray& newData);
|
||||
|
||||
signals:
|
||||
void tokensAdded(const QString& delta);
|
||||
void toolCallProposed(const KIToolCall& call);
|
||||
void toolResultRequested(const KIToolCall& call);
|
||||
void traceEvent(const QVariantMap& span);
|
||||
void finished();
|
||||
void errorOccurred(const KIError& error);
|
||||
void finishedChanged(); // Added this signal
|
||||
|
||||
protected:
|
||||
bool m_finished = false;
|
||||
int m_promptTokens = 0;
|
||||
int m_completionTokens = 0;
|
||||
QString m_model;
|
||||
QByteArray m_buffer; // Added buffer for streaming
|
||||
QString m_accumulatedText; // Added for accumulating text
|
||||
};
|
||||
|
||||
} // namespace KI
|
||||
|
||||
#endif // KIANIREPLY_H
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
|
||||
#ifndef KIANIEMBEDDING_H
|
||||
#define KIANIEMBEDDING_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
namespace KI {
|
||||
|
||||
/**
|
||||
* KIEmbedOptions and KIEmbeddingResult document the embedding API exposed by libKI providers.
|
||||
*
|
||||
* Semantics
|
||||
* - Providers should accept one or many input texts and return one vector per input.
|
||||
* - The `model` is a free-form identifier understood by the provider (e.g., "bge-m3:latest").
|
||||
* - If `normalize` is set to "l2", providers may L2-normalize vectors client-side for cosine search.
|
||||
*/
|
||||
|
||||
class KIEmbedOptions
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString model MEMBER model)
|
||||
Q_PROPERTY(QString normalize MEMBER normalize)
|
||||
|
||||
public:
|
||||
QString model = "text-embed-local";
|
||||
QString normalize = "l2";
|
||||
|
||||
bool operator==(const KIEmbedOptions& other) const = default;
|
||||
bool operator!=(const KIEmbedOptions& other) const = default;
|
||||
};
|
||||
|
||||
class KIEmbeddingResult
|
||||
{
|
||||
Q_GADGET
|
||||
|
||||
public:
|
||||
QVector<QVector<float>> vectors;
|
||||
QString model;
|
||||
|
||||
bool operator==(const KIEmbeddingResult& other) const = default;
|
||||
bool operator!=(const KIEmbeddingResult& other) const = default;
|
||||
};
|
||||
|
||||
} // namespace KI
|
||||
|
||||
#endif // KIANIEMBEDDING_H
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
|
||||
#ifndef KIANIMESSAGE_H
|
||||
#define KIANIMESSAGE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
#include <QVariantMap>
|
||||
|
||||
namespace KI {
|
||||
|
||||
class KIMessagePart
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString mime MEMBER mime)
|
||||
Q_PROPERTY(QString text MEMBER text) // for text/plain
|
||||
// future: binary, image refs, etc.
|
||||
public:
|
||||
QString mime; // "text/plain", "application/json"
|
||||
QString text;
|
||||
|
||||
bool operator==(const KIMessagePart& other) const = default;
|
||||
bool operator!=(const KIMessagePart& other) const = default;
|
||||
};
|
||||
|
||||
class KIMessage
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString role MEMBER role) // "system" | "user" | "assistant" | "tool"
|
||||
Q_PROPERTY(QList<KIMessagePart> parts MEMBER parts)
|
||||
public:
|
||||
QString role;
|
||||
QList<KIMessagePart> parts;
|
||||
QVariantMap metadata; // arbitrary
|
||||
|
||||
bool operator==(const KIMessage& other) const = default;
|
||||
bool operator!=(const KIMessage& other) const = default;
|
||||
};
|
||||
|
||||
} // namespace KI
|
||||
|
||||
#endif // KIANIMESSAGE_H
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
|
||||
#ifndef KIANITHREAD_H
|
||||
#define KIANITHREAD_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include "KIMessage.h"
|
||||
|
||||
namespace KI {
|
||||
|
||||
class KIThread
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QList<KIMessage> messages MEMBER messages)
|
||||
|
||||
public:
|
||||
QList<KIMessage> messages;
|
||||
|
||||
bool operator==(const KIThread& other) const = default;
|
||||
bool operator!=(const KIThread& other) const = default;
|
||||
};
|
||||
|
||||
} // namespace KI
|
||||
|
||||
#endif // KIANITHREAD_H
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
#ifndef KIANIPOLICY_H
|
||||
#define KIANIPOLICY_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace KI {
|
||||
|
||||
class KIPolicy
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString visibility MEMBER visibility)
|
||||
Q_PROPERTY(bool allowNetwork MEMBER allowNetwork)
|
||||
Q_PROPERTY(QStringList redactions MEMBER redactions)
|
||||
|
||||
public:
|
||||
QString visibility = "private";
|
||||
bool allowNetwork = false;
|
||||
QStringList redactions;
|
||||
|
||||
bool operator==(const KIPolicy& other) const = default;
|
||||
bool operator!=(const KIPolicy& other) const = default;
|
||||
};
|
||||
|
||||
} // namespace KI
|
||||
|
||||
#endif // KIANIPOLICY_H
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
#ifndef KIANICAPABILITIES_H
|
||||
#define KIANICAPABILITIES_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
namespace KI {
|
||||
|
||||
class KICapabilities : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool jsonMode MEMBER m_jsonMode CONSTANT)
|
||||
Q_PROPERTY(bool functionCalling MEMBER m_functionCalling CONSTANT)
|
||||
Q_PROPERTY(bool systemPrompts MEMBER m_systemPrompts CONSTANT)
|
||||
Q_PROPERTY(bool logprobs MEMBER m_logprobs CONSTANT)
|
||||
Q_PROPERTY(bool images MEMBER m_images CONSTANT)
|
||||
|
||||
public:
|
||||
KICapabilities(QObject *parent = nullptr) : QObject(parent) {}
|
||||
|
||||
bool jsonMode() const { return m_jsonMode; }
|
||||
bool functionCalling() const { return m_functionCalling; }
|
||||
bool systemPrompts() const { return m_systemPrompts; }
|
||||
bool logprobs() const { return m_logprobs; }
|
||||
bool images() const { return m_images; }
|
||||
|
||||
void setJsonMode(bool jsonMode) { m_jsonMode = jsonMode; }
|
||||
void setFunctionCalling(bool functionCalling) { m_functionCalling = functionCalling; }
|
||||
void setSystemPrompts(bool systemPrompts) { m_systemPrompts = systemPrompts; }
|
||||
void setLogprobs(bool logprobs) { m_logprobs = logprobs; }
|
||||
void setImages(bool images) { m_images = images; }
|
||||
|
||||
private:
|
||||
bool m_jsonMode = false;
|
||||
bool m_functionCalling = false;
|
||||
bool m_systemPrompts = false;
|
||||
bool m_logprobs = false;
|
||||
bool m_images = false;
|
||||
};
|
||||
|
||||
} // namespace KI
|
||||
|
||||
#endif // KIANICAPABILITIES_H
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
#ifndef KIANIPROVIDER_H
|
||||
#define KIANIPROVIDER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
#include <QFuture>
|
||||
#include "../Message/KIThread.h"
|
||||
#include "../Completion/KIReply.h"
|
||||
#include "../Embedding/KIEmbedding.h"
|
||||
#include "KICapabilities.h"
|
||||
|
||||
namespace KI {
|
||||
|
||||
class KIChatOptions; // Forward declaration
|
||||
class KIEmbedOptions; // Forward declaration
|
||||
|
||||
class KIProvider : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString name READ name CONSTANT)
|
||||
Q_PROPERTY(QStringList models READ models NOTIFY modelsChanged)
|
||||
Q_PROPERTY(KICapabilities* caps READ caps CONSTANT)
|
||||
|
||||
public:
|
||||
explicit KIProvider(QObject *parent = nullptr) : QObject(parent) {}
|
||||
|
||||
virtual QString name() const = 0;
|
||||
virtual QStringList models() const = 0;
|
||||
virtual KICapabilities* caps() const = 0;
|
||||
|
||||
virtual QFuture<KIReply*> chat(const KIThread& thread, const KIChatOptions& opts) = 0;
|
||||
virtual QFuture<KIEmbeddingResult> embed(const QStringList& texts, const KIEmbedOptions& opts) = 0;
|
||||
virtual void cancel(quint64 requestId) = 0;
|
||||
|
||||
signals:
|
||||
void modelsChanged();
|
||||
};
|
||||
|
||||
} // namespace KI
|
||||
|
||||
#endif // KIANIPROVIDER_H
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
#include "OllamaProvider.h"
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include "../Completion/KIChatOptions.h" // Added
|
||||
#include "../Completion/KIError.h" // Added
|
||||
|
||||
namespace KI {
|
||||
|
||||
OllamaProvider::OllamaProvider(QObject *parent) : KIProvider(parent)
|
||||
{
|
||||
m_manager = new QNetworkAccessManager(this);
|
||||
m_caps = new KICapabilities(this);
|
||||
m_caps->setJsonMode(true);
|
||||
m_caps->setFunctionCalling(true);
|
||||
m_caps->setSystemPrompts(true);
|
||||
reload();
|
||||
}
|
||||
|
||||
QString OllamaProvider::name() const
|
||||
{
|
||||
return "Ollama";
|
||||
}
|
||||
|
||||
QStringList OllamaProvider::models() const
|
||||
{
|
||||
return m_models;
|
||||
}
|
||||
|
||||
KICapabilities* OllamaProvider::caps() const
|
||||
{
|
||||
return m_caps;
|
||||
}
|
||||
|
||||
static QString ollamaBaseUrl() {
|
||||
const QByteArray env = qgetenv("OLLAMA_BASE");
|
||||
if (!env.isEmpty()) return QString::fromLocal8Bit(env);
|
||||
return QStringLiteral("http://localhost:11434");
|
||||
}
|
||||
|
||||
void OllamaProvider::reload()
|
||||
{
|
||||
QNetworkRequest req{QUrl(ollamaBaseUrl() + QStringLiteral("/api/tags"))};
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||
auto rep = m_manager->get(req);
|
||||
connect(rep, &QNetworkReply::finished, this, [this, rep] {
|
||||
if (rep->error() != QNetworkReply::NoError) {
|
||||
// TODO: Handle error
|
||||
return;
|
||||
}
|
||||
|
||||
const auto json = QJsonDocument::fromJson(rep->readAll());
|
||||
const auto models = json["models"].toArray();
|
||||
for (const QJsonValue &model : models) {
|
||||
m_models.push_back(model["name"].toString());
|
||||
}
|
||||
emit modelsChanged();
|
||||
rep->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
QFuture<KIReply*> OllamaProvider::chat(const KIThread& thread, const KIChatOptions& opts)
|
||||
{
|
||||
QNetworkRequest req{QUrl(ollamaBaseUrl() + QStringLiteral("/api/generate"))};
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||
|
||||
QJsonObject data;
|
||||
data["model"] = opts.model;
|
||||
QString prompt;
|
||||
for (const auto& message : thread.messages) {
|
||||
if (message.role == "system") {
|
||||
prompt += "[SYSTEM] " + message.parts.first().text + "\n";
|
||||
} else if (message.role == "user") {
|
||||
prompt += "[USER] " + message.parts.first().text + "\n";
|
||||
} else if (message.role == "assistant") {
|
||||
prompt += "[ASSISTANT] " + message.parts.first().text + "\n";
|
||||
}
|
||||
}
|
||||
data["prompt"] = prompt;
|
||||
|
||||
if (opts.stream) {
|
||||
data["stream"] = true;
|
||||
}
|
||||
|
||||
if (opts.jsonMode) {
|
||||
data["format"] = "json";
|
||||
}
|
||||
|
||||
// TODO: Add other options
|
||||
|
||||
auto reply = new KIReply();
|
||||
|
||||
auto netReply = m_manager->post(req, QJsonDocument(data).toJson());
|
||||
|
||||
connect(netReply, &QNetworkReply::readyRead, reply, [reply, netReply]() {
|
||||
reply->processIncomingData(netReply->readAll());
|
||||
});
|
||||
|
||||
connect(netReply, &QNetworkReply::finished, reply, [reply, netReply]() {
|
||||
if (netReply->error() != QNetworkReply::NoError) {
|
||||
KIError error;
|
||||
error.code = KIError::NetworkError;
|
||||
error.message = netReply->errorString();
|
||||
reply->setError(error);
|
||||
} else {
|
||||
reply->setFinished(true);
|
||||
}
|
||||
netReply->deleteLater();
|
||||
});
|
||||
|
||||
return QtFuture::makeReadyFuture(reply);
|
||||
}
|
||||
|
||||
QFuture<KIEmbeddingResult> OllamaProvider::embed(const QStringList& texts, const KIEmbedOptions& opts)
|
||||
{
|
||||
// Execute one request per input text; aggregate outputs.
|
||||
QFutureInterface<KIEmbeddingResult> fi;
|
||||
fi.reportStarted();
|
||||
if (texts.isEmpty()) { KIEmbeddingResult r; r.model = opts.model; fi.reportResult(r); fi.reportFinished(); return fi.future(); }
|
||||
|
||||
struct Accum { QVector<QVector<float>> vectors; int remaining = 0; QString model; };
|
||||
auto acc = new Accum();
|
||||
acc->vectors.resize(texts.size());
|
||||
acc->remaining = texts.size();
|
||||
|
||||
const QUrl url(ollamaBaseUrl() + QStringLiteral("/api/embeddings"));
|
||||
for (int i = 0; i < texts.size(); ++i) {
|
||||
QNetworkRequest req{url};
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||
const QJsonObject body{ {QStringLiteral("model"), opts.model}, {QStringLiteral("prompt"), texts[i]} };
|
||||
auto rep = m_manager->post(req, QJsonDocument(body).toJson());
|
||||
connect(rep, &QNetworkReply::finished, this, [rep, i, acc, fi]() mutable {
|
||||
if (rep->error() == QNetworkReply::NoError) {
|
||||
const auto obj = QJsonDocument::fromJson(rep->readAll()).object();
|
||||
if (acc->model.isEmpty()) acc->model = obj.value(QStringLiteral("model")).toString();
|
||||
const auto arr = obj.value(QStringLiteral("embedding")).toArray();
|
||||
QVector<float> vec; vec.reserve(arr.size());
|
||||
for (const auto &v : arr) vec.push_back(static_cast<float>(v.toDouble()));
|
||||
acc->vectors[i] = std::move(vec);
|
||||
}
|
||||
rep->deleteLater();
|
||||
acc->remaining -= 1;
|
||||
if (acc->remaining == 0) {
|
||||
KIEmbeddingResult res; res.vectors = std::move(acc->vectors); res.model = acc->model;
|
||||
fi.reportResult(res);
|
||||
fi.reportFinished();
|
||||
delete acc;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return fi.future();
|
||||
}
|
||||
|
||||
void OllamaProvider::cancel(quint64 requestId)
|
||||
{
|
||||
Q_UNUSED(requestId);
|
||||
// TODO: Implement cancellation logic
|
||||
}
|
||||
|
||||
} // namespace KI
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
|
||||
#ifndef OLLAMAPROVIDER_H
|
||||
#define OLLAMAPROVIDER_H
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
#include "KIProvider.h"
|
||||
#include "../Completion/KIChatOptions.h" // Included full definition
|
||||
#include "../Embedding/KIEmbedding.h" // Included full definition for KIEmbedOptions and KIEmbeddingResult
|
||||
#include "../Completion/KIReply.h" // Included full definition for KIReply (needed for QFuture<KIReply*>)
|
||||
#include "../Message/KIThread.h" // Included full definition for KIThread
|
||||
#include "KICapabilities.h"
|
||||
|
||||
namespace KI {
|
||||
|
||||
class OllamaProvider : public KIProvider
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit OllamaProvider(QObject *parent = nullptr);
|
||||
|
||||
QString name() const override;
|
||||
QStringList models() const override;
|
||||
KICapabilities* caps() const override;
|
||||
|
||||
QFuture<KIReply*> chat(const KIThread& thread, const KIChatOptions& opts) override;
|
||||
QFuture<KIEmbeddingResult> embed(const QStringList& texts, const KIEmbedOptions& opts) override;
|
||||
void cancel(quint64 requestId) override;
|
||||
|
||||
public slots:
|
||||
void reload();
|
||||
|
||||
private:
|
||||
QNetworkAccessManager* m_manager;
|
||||
QStringList m_models;
|
||||
KICapabilities* m_caps;
|
||||
};
|
||||
|
||||
} // namespace KI
|
||||
|
||||
#endif // OLLAMAPROVIDER_H
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
|
||||
#ifndef KIANITOOL_H
|
||||
#define KIANITOOL_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
|
||||
namespace KI {
|
||||
|
||||
class KIToolParam
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString name MEMBER name)
|
||||
Q_PROPERTY(QString type MEMBER type)
|
||||
Q_PROPERTY(bool required MEMBER required)
|
||||
Q_PROPERTY(QVariant defaultValue MEMBER defaultValue)
|
||||
|
||||
public:
|
||||
QString name, type;
|
||||
bool required = false;
|
||||
QVariant defaultValue;
|
||||
|
||||
bool operator==(const KIToolParam& other) const = default;
|
||||
bool operator!=(const KIToolParam& other) const = default;
|
||||
};
|
||||
|
||||
class KIToolSpec
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString name MEMBER name)
|
||||
Q_PROPERTY(QString description MEMBER description)
|
||||
Q_PROPERTY(QList<KIToolParam> params MEMBER params)
|
||||
|
||||
public:
|
||||
QString name, description;
|
||||
QList<KIToolParam> params;
|
||||
|
||||
bool operator==(const KIToolSpec& other) const = default;
|
||||
bool operator!=(const KIToolSpec& other) const = default;
|
||||
};
|
||||
|
||||
class KIToolCall
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString name MEMBER name)
|
||||
Q_PROPERTY(QVariantMap arguments MEMBER arguments)
|
||||
|
||||
public:
|
||||
QString name;
|
||||
QVariantMap arguments;
|
||||
|
||||
bool operator==(const KIToolCall& other) const = default;
|
||||
bool operator!=(const KIToolCall& other) const = default;
|
||||
};
|
||||
|
||||
class KIToolResult
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString name MEMBER name)
|
||||
Q_PROPERTY(QVariant result MEMBER result)
|
||||
|
||||
public:
|
||||
QString name;
|
||||
QVariant result;
|
||||
|
||||
bool operator==(const KIToolResult& other) const = default;
|
||||
bool operator!=(const KIToolResult& other) const = default;
|
||||
};
|
||||
|
||||
} // namespace KI
|
||||
|
||||
#endif // KIANITOOL_H
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
add_executable(kompanion
|
||||
KompanionApp.cpp
|
||||
)
|
||||
target_include_directories(kompanion PRIVATE ../)
|
||||
target_link_libraries(kompanion PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Sql
|
||||
KF6::ConfigCore
|
||||
kom_dal
|
||||
kompanion_mw
|
||||
kom_ki
|
||||
kom_mcp
|
||||
Qt6::McpServer
|
||||
)
|
||||
install(TARGETS kompanion RUNTIME ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,20 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Moved from ingest/run_ingest.py for transparency. See ingest/pipeline.qt-kde-bge-m3.yaml
|
||||
for configuration fields. This script remains a reference pipeline and is not
|
||||
used by the C++ build.
|
||||
"""
|
||||
# Original content is available under ingest/run_ingest.py. Keeping this as a thin
|
||||
# forwarder/import to avoid duplication while surfacing the script under src/cli/.
|
||||
import os, sys
|
||||
from pathlib import Path
|
||||
|
||||
here = Path(__file__).resolve()
|
||||
ingest_script = here.parent.parent.parent / 'ingest' / 'run_ingest.py'
|
||||
if not ingest_script.exists():
|
||||
print('ingest/run_ingest.py not found', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
code = ingest_script.read_text(encoding='utf-8')
|
||||
exec(compile(code, str(ingest_script), 'exec'))
|
||||
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Lightweight embedding helper moved from ingest/ for transparency.
|
||||
|
||||
Usage examples:
|
||||
- Single embedding via Ollama:
|
||||
OLLAMA_BASE=http://localhost:11434 \
|
||||
./py_embedder.py --model bge-m3:latest --text "hello world"
|
||||
|
||||
- Batch from stdin (one line per text):
|
||||
./py_embedder.py --model bge-m3:latest --stdin < texts.txt
|
||||
|
||||
Outputs JSON array of floats (for single text) or array-of-arrays for batches.
|
||||
This script does not touch the database; it only produces vectors.
|
||||
"""
|
||||
import os, sys, json, argparse, requests
|
||||
|
||||
def embed_ollama(texts, model, base):
|
||||
url = f"{base}/api/embeddings"
|
||||
# Some Ollama models accept a single prompt; do one-by-one for reliability
|
||||
out = []
|
||||
for t in texts:
|
||||
r = requests.post(url, json={"model": model, "prompt": t}, timeout=120)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
if "embedding" in data:
|
||||
out.append(data["embedding"]) # single vector
|
||||
elif "embeddings" in data:
|
||||
out.extend(data["embeddings"]) # multiple vectors
|
||||
else:
|
||||
raise RuntimeError("Embedding response missing 'embedding(s)'")
|
||||
return out
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--model", default=os.environ.get("EMBED_MODEL","bge-m3:latest"))
|
||||
ap.add_argument("--text", help="Text to embed; if omitted, use --stdin")
|
||||
ap.add_argument("--stdin", action="store_true", help="Read texts from stdin (one per line)")
|
||||
ap.add_argument("--base", default=os.environ.get("OLLAMA_BASE","http://localhost:11434"))
|
||||
args = ap.parse_args()
|
||||
|
||||
texts = []
|
||||
if args.text:
|
||||
texts = [args.text]
|
||||
elif args.stdin:
|
||||
texts = [line.rstrip("\n") for line in sys.stdin if line.strip()]
|
||||
else:
|
||||
ap.error("Provide --text or --stdin")
|
||||
|
||||
vectors = embed_ollama(texts, args.model, args.base)
|
||||
if len(texts) == 1 and vectors:
|
||||
print(json.dumps(vectors[0]))
|
||||
else:
|
||||
print(json.dumps(vectors))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
add_library(kom_dal STATIC
|
||||
PgDal.cpp
|
||||
)
|
||||
|
||||
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)
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
#pragma once
|
||||
#include "Models.hpp"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include <optional>
|
||||
|
||||
namespace ki {
|
||||
class IDatabase {
|
||||
public:
|
||||
virtual ~IDatabase() = default;
|
||||
// Upsert item by (namespace,key); returns {item_id, new_revision}.
|
||||
virtual std::pair<std::string,int> upsertItem(
|
||||
const std::string& namespace_id,
|
||||
const std::optional<std::string>& key,
|
||||
const std::string& content,
|
||||
const std::string& metadata_json,
|
||||
const std::vector<std::string>& tags) = 0;
|
||||
|
||||
// Insert a chunk; returns chunk_id.
|
||||
virtual std::string insertChunk(const std::string& item_id, int seq, const std::string& content) = 0;
|
||||
|
||||
// Insert an embedding for a chunk.
|
||||
virtual void insertEmbedding(const Embedding& e) = 0;
|
||||
|
||||
// Hybrid search. Returns chunk_ids ordered by relevance.
|
||||
virtual std::vector<std::string> hybridSearch(
|
||||
const std::vector<float>& query_vec,
|
||||
const std::string& model,
|
||||
const std::string& namespace_id,
|
||||
const std::string& query_text,
|
||||
int k) = 0;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <cstdint>
|
||||
|
||||
namespace ki {
|
||||
struct MemoryItem {
|
||||
std::string id;
|
||||
std::string namespace_id;
|
||||
std::optional<std::string> key;
|
||||
std::string content;
|
||||
std::string metadata_json;
|
||||
std::vector<std::string> tags;
|
||||
int revision = 1;
|
||||
};
|
||||
|
||||
struct MemoryChunk {
|
||||
std::string id;
|
||||
std::string item_id;
|
||||
int seq = 0;
|
||||
std::string content;
|
||||
};
|
||||
|
||||
struct Embedding {
|
||||
std::string chunk_id;
|
||||
std::string model;
|
||||
int dim = 1536;
|
||||
std::vector<float> vector;
|
||||
};
|
||||
}
|
||||
1108
src/dal/PgDal.cpp
1108
src/dal/PgDal.cpp
File diff suppressed because it is too large
Load Diff
|
|
@ -1,180 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "IDatabase.hpp"
|
||||
|
||||
#include <QSqlDatabase>
|
||||
#include <QString>
|
||||
#include <QCryptographicHash>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace ki {
|
||||
|
||||
struct NamespaceRow {
|
||||
std::string id;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
struct ItemRow {
|
||||
std::string id;
|
||||
std::string namespace_id;
|
||||
std::optional<std::string> key;
|
||||
std::string content_json;
|
||||
std::string metadata_json = "{}";
|
||||
std::optional<std::string> text;
|
||||
std::vector<std::string> tags;
|
||||
int revision = 1;
|
||||
std::chrono::system_clock::time_point created_at;
|
||||
std::optional<std::chrono::system_clock::time_point> expires_at;
|
||||
};
|
||||
|
||||
struct ChunkRow {
|
||||
std::string id;
|
||||
std::string item_id;
|
||||
int ord = 0;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
struct EmbeddingRow {
|
||||
std::string id;
|
||||
std::string chunk_id;
|
||||
std::string model;
|
||||
int dim = 0;
|
||||
std::vector<float> vector;
|
||||
};
|
||||
|
||||
struct AuthSecret {
|
||||
std::string secret_hash;
|
||||
};
|
||||
|
||||
class PgDal final : public IDatabase {
|
||||
public:
|
||||
PgDal();
|
||||
~PgDal();
|
||||
|
||||
bool connect(const std::string& dsn);
|
||||
bool begin();
|
||||
void commit();
|
||||
void rollback();
|
||||
|
||||
std::pair<NamespaceRow, std::string> createNamespaceWithSecret(const std::string& name);
|
||||
std::optional<NamespaceRow> ensureNamespace(const std::string& name);
|
||||
std::optional<NamespaceRow> findNamespace(const std::string& name) const;
|
||||
std::optional<AuthSecret> findSecretByNamespaceId(const std::string& namespaceId) const;
|
||||
|
||||
std::string upsertItem(const ItemRow& row);
|
||||
std::vector<std::string> upsertChunks(const std::vector<ChunkRow>& chunks);
|
||||
void upsertEmbeddings(const std::vector<EmbeddingRow>& embeddings);
|
||||
|
||||
std::vector<ItemRow> searchText(const std::string& namespaceId,
|
||||
const std::string& query,
|
||||
int limit);
|
||||
std::vector<std::pair<std::string, float>> searchVector(
|
||||
const std::string& namespaceId,
|
||||
const std::vector<float>& embedding,
|
||||
int limit);
|
||||
std::optional<ItemRow> getItemById(const std::string& id) const;
|
||||
std::vector<ItemRow> fetchContext(const std::string& namespaceId,
|
||||
const std::optional<std::string>& key,
|
||||
const std::vector<std::string>& tags,
|
||||
const std::optional<std::string>& sinceIso,
|
||||
int limit);
|
||||
|
||||
// IDatabase overrides
|
||||
std::pair<std::string, int> upsertItem(
|
||||
const std::string& namespace_id,
|
||||
const std::optional<std::string>& key,
|
||||
const std::string& content,
|
||||
const std::string& metadata_json,
|
||||
const std::vector<std::string>& tags) override;
|
||||
std::string insertChunk(const std::string& item_id,
|
||||
int seq,
|
||||
const std::string& content) override;
|
||||
void insertEmbedding(const Embedding& embedding) override;
|
||||
std::vector<std::string> hybridSearch(const std::vector<float>& query_vec,
|
||||
const std::string& model,
|
||||
const std::string& namespace_id,
|
||||
const std::string& query_text,
|
||||
int k) override;
|
||||
|
||||
private:
|
||||
struct ConnectionConfig {
|
||||
QString host;
|
||||
int port = 5432;
|
||||
QString dbname;
|
||||
QString user;
|
||||
QString password;
|
||||
bool useSocket = false;
|
||||
QString socketPath;
|
||||
QString options;
|
||||
};
|
||||
|
||||
bool hasDatabase() const;
|
||||
bool openDatabase(const std::string& dsn);
|
||||
void closeDatabase();
|
||||
QSqlDatabase database() const;
|
||||
ConnectionConfig parseDsn(const std::string& dsn) const;
|
||||
|
||||
std::pair<NamespaceRow, std::string> sqlCreateNamespaceWithSecret(const std::string& name);
|
||||
std::optional<NamespaceRow> sqlEnsureNamespace(const std::string& name);
|
||||
std::optional<NamespaceRow> sqlFindNamespace(const std::string& name) const;
|
||||
std::optional<AuthSecret> sqlFindSecretByNamespaceId(const std::string& namespaceId) const;
|
||||
void sqlInsertSecret(const std::string& namespaceId, const std::string& secretHash);
|
||||
std::pair<std::string, int> sqlUpsertItem(const ItemRow& row);
|
||||
std::vector<std::string> sqlUpsertChunks(const std::vector<ChunkRow>& chunks);
|
||||
void sqlUpsertEmbeddings(const std::vector<EmbeddingRow>& embeddings);
|
||||
std::vector<ItemRow> sqlSearchText(const std::string& namespaceId,
|
||||
const std::string& query,
|
||||
int limit) const;
|
||||
std::vector<std::pair<std::string, float>> sqlSearchVector(
|
||||
const std::string& namespaceId,
|
||||
const std::vector<float>& embedding,
|
||||
int limit) const;
|
||||
std::optional<ItemRow> sqlGetItemById(const std::string& id) const;
|
||||
std::vector<ItemRow> sqlFetchContext(const std::string& namespaceId,
|
||||
const std::optional<std::string>& key,
|
||||
const std::vector<std::string>& tags,
|
||||
const std::optional<std::string>& sinceIso,
|
||||
int limit) const;
|
||||
|
||||
std::vector<std::string> sqlHybridSearch(const std::vector<float>& query_vec,
|
||||
const std::string& model,
|
||||
const std::string& namespace_id,
|
||||
const std::string& query_text,
|
||||
int k);
|
||||
|
||||
std::string allocateId(std::size_t& counter, const std::string& prefix);
|
||||
static std::string toLower(const std::string& value);
|
||||
static bool isStubDsn(const std::string& dsn);
|
||||
static std::string escapePgArrayElement(const std::string& value);
|
||||
static std::string toPgArrayLiteral(const std::vector<std::string>& values);
|
||||
static std::string toPgVectorLiteral(const std::vector<float>& values);
|
||||
static std::vector<std::string> parsePgTextArray(const QString& value);
|
||||
|
||||
bool connected_ = false;
|
||||
bool useInMemory_ = true;
|
||||
std::string dsn_;
|
||||
QString connectionName_;
|
||||
bool transactionActive_ = false;
|
||||
|
||||
std::size_t nextNamespaceId_ = 1;
|
||||
std::size_t nextItemId_ = 1;
|
||||
std::size_t nextChunkId_ = 1;
|
||||
std::size_t nextEmbeddingId_ = 1;
|
||||
|
||||
std::unordered_map<std::string, NamespaceRow> namespacesByName_;
|
||||
std::unordered_map<std::string, NamespaceRow> namespacesById_;
|
||||
std::unordered_map<std::string, ItemRow> items_;
|
||||
std::unordered_map<std::string, std::vector<std::string>> itemsByNamespace_;
|
||||
std::unordered_map<std::string, ChunkRow> chunks_;
|
||||
std::unordered_map<std::string, std::vector<std::string>> chunksByItem_;
|
||||
std::unordered_map<std::string, EmbeddingRow> embeddings_;
|
||||
};
|
||||
|
||||
} // namespace ki
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
include_directories($CMAKE_SRC_DIR/src)
|
||||
add_executable(kompanion_gui
|
||||
MainWindow.cpp
|
||||
)
|
||||
target_include_directories(kompanion_gui PRIVATE ${PROJECT_SOURCE_DIR}/src)
|
||||
|
||||
target_link_libraries(kompanion_gui PRIVATE
|
||||
KF6::Parts
|
||||
KF6::TextEditor
|
||||
KF6::ConfigCore
|
||||
Qt6::McpServer
|
||||
Qt6::McpCommon
|
||||
kom_dal
|
||||
kom_ki
|
||||
)
|
||||
|
||||
install(TARGETS kompanion_gui RUNTIME DESTINATION bin)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue