Compare commits

...

61 Commits

Author SHA1 Message Date
Χγφτ Kompanion c1d8c582f9 Move HandlerMemory up again.
--
2025-10-20 00:17:33 +02:00
Χγφτ Kompanion d39d20dd22 Add development set up variables 2025-10-19 21:09:22 +02:00
Χγφτ Kompanion 3f1410a095 Fix compile of command line parser
This is such an antiattren, amazing.
2025-10-19 19:13:25 +02:00
Χγφτ Kompanion fd922fe747 Acctually build the moved directories
Linking worked only because it was done against old binaries.
There is stil aseccuring in recur;
2025-10-19 19:11:49 +02:00
Χγφτ Kompanion 4091059e75 Fix production daabase creation
* The user should be externally configured.
2025-10-19 19:10:54 +02:00
Χγφτ Kompanion b2197ff11c Look for MCP-Server for test instantiation 2025-10-19 19:08:40 +02:00
Χγφτ Kompanion 720d445f1f cli: add --warm-cache (policy/ad-hoc) and --rehydrate; mcp: warm_cache accepts since/tags/key; add delete_context (stub) and upsert_and_embed wiring; resources: basic NL regex mappings 2025-10-19 11:56:14 +02:00
Χγφτ Kompanion 6fd938d4f3 mcp: add upsert_and_embed tool; implement warm_cache with filters + embeddings via libKI; DAL: make memory_chunks upsert unique by (item,seq) with SQL and in-memory; DB: add unique index on (item,seq) 2025-10-19 11:09:15 +02:00
Χγφτ Kompanion a0f5dd8b4f mcp: implement warm_cache using libKI embeddings + DAL upserts (chunks + embeddings); returns queued count 2025-10-19 10:55:21 +02:00
Χγφτ Kompanion d5226fe7d6 tests: add snapshot round-trip test using Orchestrator::saveSnapshot/loadSnapshot; Orchestrator reuses a DAL handle so stub mode persists within instance 2025-10-19 10:49:12 +02:00
Χγφτ Kompanion c36ef7e46b middleware: add snapshot helpers (save/load) backed by PgDal; link kom_dal with PIC; CLI: add --snapshot-save/--snapshot-load for easy syntax 2025-10-19 10:40:33 +02:00
Χγφτ Kompanion 70bd4ea064 tests: prefer stdio MCP + mcp-proxy when available; source ~/dev/main/src/env.sh if present; robust cleanup 2025-10-19 10:24:03 +02:00
Χγφτ Kompanion 6ff51da0e2 mcp: document handlers and route embed_text via libKI (OllamaProvider) with synchronous QFuture wait; ki: respect OLLAMA_BASE for tags/generate/embeddings 2025-10-19 10:23:38 +02:00
Χγφτ Kompanion 50ecdcf56b cli: surface ingest/run_ingest.py as src/cli/embed_pipeline.py to make the Python embedder visible alongside the CLI 2025-10-19 08:34:19 +02:00
Χγφτ Kompanion 924f5c7995 ki: add inline API doc for KIEmbedOptions/KIEmbeddingResult to prefer middleware-sourced documentation 2025-10-19 08:34:04 +02:00
Χγφτ Kompanion 1e15a96bbe gui: add 'Embed' button to call KIClient::embed and display resulting vector dimension 2025-10-19 08:33:52 +02:00
Χγφτ Kompanion 2ee757b64a ki: implement multi-text embeddings in OllamaProvider::embed by batching one request per text and aggregating results 2025-10-19 08:33:27 +02:00
Χγφτ Kompanion e1eb3afacc middleware: add C++ orchestrator + API docs; cli: add MCP server mode and DB navigation; tests: add orchestrator QtTest; docs: add CLI usage; db: move SQL from sql/ to db/sql; tools: remove extra python embedding stub
- Add src/middleware/orchestrator.{h,cpp} with simple journal.from_prompt task loop and Ollama-backed model provider.
- Extend src/cli/KompanionApp.cpp with:
  - MCP server mode (--mcp-serve/--mcp-address)
  - DB helpers (--db-namespaces, --db-items, --db-search, --embedding-file)
- Add tests/test_orchestrator.cpp and link Qt6::Network; keep tests passing.
- Move legacy SQL from sql/pg/001_init.sql to db/sql/pg/001_init.sql for consistency.
- Add docs/cli-and-schema-navigation.md with examples for server, tools, and schema navigation.
- Remove tools/pg_search.py (redundant embedding stub).
2025-10-19 08:32:43 +02:00
Χγφτ Kompanion 99614bbe93 Update task 2025-10-19 07:43:28 +02:00
Χγφτ Kompanion 8dcdd92055 Add middleware layer and documentation 2025-10-18 20:12:49 +02:00
Χγφτ Kompanion dadb0af954 Change API naming to KI
It is just the catchy thing to do.
2025-10-18 14:16:16 +02:00
Χγφτ Kompanion ee37ed47bb Finish the kllm integration better
Thanks to Gemini we now have a cleaner build interface
2025-10-18 14:00:09 +02:00
Χγφτ Kompanion 2ecb214510 Add full integration tests and more debug outout 2025-10-18 09:37:32 +02:00
Χγφτ Kompanion d6640abcbd Ifx path to stestscript again - remove obsolete files 2025-10-18 09:35:37 +02:00
Χγφτ Kompanion 799f060204 Tests: Make them callable from any path 2025-10-18 05:05:58 +02:00
Χγφτ Kompanion a943ca055a Refactoring move API in subdir 2025-10-17 12:50:18 +02:00
Χγφτ Kompanion b5c3dd21e0 Use new embedding size in schema 2025-10-17 02:35:59 +02:00
Χγφτ Kompanion 3ab1089c51 Adapt ingest dir to new ingest system 2025-10-17 02:30:22 +02:00
Χγφτ Kompanion 7b27493656 Add missing ingest files 2025-10-17 00:00:45 +02:00
Χγφτ Kompanion 0264262742 Adapt defaults for ingest_dir 2025-10-16 22:36:12 +02:00
Χγφτ Kompanion 4b2f9ea184 Merge branch 'work/API' to switch to KompanionAi API 2025-10-16 21:59:51 +02:00
Χγφτ Kompanion f2f7879f42 Add bearer authentication based on namespace
While there can be namespaces without a secret, commonly
the namespace should basically be the username and the
password the login secret.
2025-10-16 21:57:50 +02:00
Χγφτ Kompanion a8694850b1 Refactor: Revert auth_token removal and add namespace to ToolSchemas.json, update tests. 2025-10-16 21:51:07 +02:00
Χγφτ Kompanion bc0ae50be5 Add some debug output and simplify overyl complex code 2025-10-16 20:43:54 +02:00
Χγφτ Kompanion ca390763e6 feat: Implement initial KompanionAI SDK structure and KLLM compatibility layer 2025-10-16 19:24:09 +02:00
Χγφτ Kompanion fd289edded Outline future plans 2025-10-16 04:55:58 +02:00
Χγφτ Kompanion f71b2ef510 Implement server logic fully 2025-10-16 04:55:18 +02:00
Χγφτ Kompanion 220be1744d Update CMakeLists to follow KDE pattern 2025-10-16 01:20:30 +02:00
Χγφτ Kompanion 122df11433 memory: remove exception parsing helpers 2025-10-15 19:13:09 +02:00
Χγφτ Kompanion 8479c23234 cli: drop exceptions from KompanionApp 2025-10-15 18:44:00 +02:00
Χγφτ Kompanion a84af5d464 doc: add anything-llm compatibility plan 2025-10-15 18:43:47 +02:00
Χγφτ Kompanion db01fb0485 Add missing files 2025-10-16 03:46:06 +13:00
Χγφτ Kompanion 70848fda6e Convert to QtSql and extend 2025-10-16 03:45:27 +13:00
Χγφτ Kompanion a04506a081 Extend command line and add db init.
This also removes obsolete schema files for clarity
2025-10-15 22:35:19 +13:00
Χγφτ Kompanion e881bc4998 First prototype 2025-10-15 15:16:56 +13:00
Χγφτ Kompanion 53a1a043c7 Add Kconfig and more tools 2025-10-15 12:07:21 +13:00
Χγφτ Kompanion 779ac57f50 Add Schema files 2025-10-15 10:39:44 +13:00
Χγφτ Kompanion 122085b1f8 Added optional Postgres support:
CMake now detects libpq/pqxx, sets HAVE_PG, and links kom_dal when available
 (CMakeLists.txt:6-24, src/dal/CMakeLists.txt:9-13); PgDal gained connection management,
    prepared statements, and guarded runtime paths while preserving the
 in-memory fallback (src/dal/PgDal.cpp:1-820, src/dal/PgDal.hpp:1-153).
  - Introduced MCP resource descriptors mirroring the Ξlope memory
model—episodic events, semantic chunks, and the semantic_sync job contract—to
 guide tooling and DAL sync behavior (resources/memory/
    kom.memory.v1/episodic.json, semantic.json, jobs/semantic_sync.json).

Note: Work done by little blue
2025-10-15 10:38:33 +13:00
Χγφτ Kompanion b567b51ee2 Snapshot commit 2025-10-15 10:14:58 +13:00
Χγφτ Kompanion 5ae22ff9f8 Add testsystem 2025-10-15 01:22:29 +13:00
Χγφτ Kompanion 2210e3a260 Add PgDal Implementation 2025-10-15 01:01:26 +13:00
Χγφτ Kompanion 93400a2d21 Add PgDal Implementation 2025-10-14 21:46:46 +13:00
Χγφτ Kompanion ba9c4c0f72 Add changes made through MCP for backup 2025-10-14 13:22:44 +13:00
Χγφτ Kompanion 0f3a56d42f docker: fix host ollama port 2025-10-13 08:17:38 +13:00
Χγφτ Kompanion f276d702b2 runtime: Update runner script 2025-10-13 07:34:03 +13:00
Χγφτ Kompanion d121f2a76d db: Add pgsql schema 2025-10-13 07:32:35 +13:00
Χγφτ Kompanion 9c63b6c593 db: core pgvector schema; docs: ASPECTS (facets of Χγφτ) 2025-10-13 06:05:32 +13:00
Χγφτ Kompanion f73d702ba6 Add kom_runner implementation 2025-10-13 05:06:39 +13:00
Χγφτ Kompanion 3ae8bebb54 Readd a self contained compose dockerfile 2025-10-13 04:57:08 +13:00
esus 628e7b529e Use host tor and ollama for now
Later for publishing we can offer both variants.
2025-10-13 04:32:19 +13:00
Χγφτ Kompanion 1585c168fd chore(docker): add host-runner compose using host ollama:11435 & tor:9051 2025-10-13 04:08:44 +13:00
143 changed files with 12059 additions and 66 deletions

264
.acf/tasks.json Normal file
View File

@ -0,0 +1,264 @@
{
"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\""
}
]
}
]
}

95
.clang-format Normal file
View File

@ -0,0 +1,95 @@
---
# 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

View File

@ -0,0 +1,7 @@
# 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 Normal file
View File

@ -0,0 +1,45 @@
# 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.

View File

@ -1,10 +1,68 @@
cmake_minimum_required(VERSION 3.16)
project(MetalKompanion LANGUAGES CXX)
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")
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Core Network DBus)
add_executable(kompanion_server
src/main.cpp
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
)
target_link_libraries(kompanion_server Qt6::Core Qt6::Network Qt6::DBus)
install(TARGETS kompanion_server RUNTIME DESTINATION bin)
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)

4
bin/compose-up-host.sh Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
docker compose -f docker/compose.host.yml up -d

1
db/init/001_roles.sql Normal file
View File

@ -0,0 +1 @@
CREATE DATABASE kompanion OWNER kompanion;

View File

@ -0,0 +1,2 @@
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_trgm;

42
db/init/010_schema.sql Normal file
View File

@ -0,0 +1,42 @@
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)
);

8
db/init/011_auth.sql Normal file
View File

@ -0,0 +1,8 @@
-- 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)
);

11
db/init/012_test_user.sql Normal file
View File

@ -0,0 +1,11 @@
-- 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 $$;

15
db/init/020_indexes.sql Normal file
View File

@ -0,0 +1,15 @@
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';

View File

@ -0,0 +1,87 @@
-- 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();

View File

@ -1,43 +0,0 @@
-- 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
);

23
db/scripts/create-prod-db.sh Executable file
View File

@ -0,0 +1,23 @@
#!/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

20
db/scripts/create-test-db.sh Executable file
View File

@ -0,0 +1,20 @@
#!/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 Normal file
View File

@ -0,0 +1,71 @@
# 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=%{time h:mm:ss.zzz}%{if-category} %{category}:%{endif} %{if-debug}%{function}%{endif}%{if-warning}%{backtrace depth=5}%{endif}%{if-critical}%{backtrace depth=3}%{endif}%{if-fatal}%{backtrace depth=3}%{endif} %{message}

22
docker/compose.host.yml Normal file
View File

@ -0,0 +1,22 @@
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"]

View File

@ -10,13 +10,11 @@ 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
@ -30,9 +28,9 @@ services:
XDG_STATE_HOME: /state
XDG_CONFIG_HOME: /config
XDG_CACHE_HOME: /cache
ALL_PROXY: socks5h://tor:9051
ALL_PROXY: socks5h://tor:9050
NO_PROXY: ollama,localhost,127.0.0.1
OLLAMA_BASE: http://ollama:11435 # talk to container by DNS name
OLLAMA_BASE: http://ollama:11434 # talk to container by DNS name
depends_on: [ollama, tor]
volumes:
- /home/kompanion/.local/state/kompanion:/state/kompanion

View File

@ -1,12 +0,0 @@
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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,375 @@
🧭 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.
LanguageTool 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._

11
docs/ASPECTS.md Normal file
View File

@ -0,0 +1,11 @@
# 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.

View File

@ -0,0 +1 @@
# Identity & Aspects (placeholder)

View File

@ -0,0 +1 @@
# Runtime vs Pattern Exchange (placeholder)

63
docs/anything-llm-eval.md Normal file
View File

@ -0,0 +1,63 @@
# 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. Kompanions 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 rows 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 Kompanions 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 Kompanions 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 Kompanions embedding pipeline to fail fast if the produced dimension differs from the tables 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 Kompanions 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.

25
docs/backup-e2ee-spec.md Normal file
View File

@ -0,0 +1,25 @@
# 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.

View File

@ -0,0 +1,32 @@
# 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 23 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.

View File

@ -0,0 +1,50 @@
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 inmemory 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 (fulltext)
- `embeddings.vector` IVFFLAT with `vector_cosine_ops` (permodel partial index)
Tips
- For quick trials without Postgres, tool calls work in stub mode (inmemory DAL). To exercise vector search and FTS, run the DB init scripts via `kompanion --init`.
- Use `kompanion --verbose` to echo JSON requests/responses.

361
docs/client-memory.md Normal file
View File

@ -0,0 +1,361 @@
Below is a **single copypastable Markdown file** that proposes a clientside architecture which treats memory as a living, hierarchical JSON **dictionaryofdictionaries** (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:** Clientside 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 highquality 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 dictionaryofdictionaries, “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**. Thats fine: well implement the cognitive part **clientside** and use the server as a persistence/search/recall backbone.
---
## 2. The mental model (HDoD = dictionaryofdictionaries)
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**
* inmemory 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 chainofthought *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 (clientside, 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 reactivated.
### 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 *microchunks*:
* **Concept**: “RAII”, “SFINAE”, “Rule of 5/0”, “ADL”, “Type erasure”
* **Pattern**: “pImpl”, “CRTP”, “Enableif idiom”
* **Snippet**: idiomatic examples (≤30 lines), compilechecked
* **Antipatterns**: “raw new/delete in modern C++”, “overusing exceptions”
* **Build/Tooling**: CMake minimum skeletons, `add_library`, interfaces, `FetchContent`
* **Test**: Catch2/GoogleTest minimal cases; propertybased 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 (todays 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 “firstgrade 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 4060% 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 pathlike**: `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 **typespecific** vectors later.
* **Edges carry weights**; they contribute to resonance.
* **Resonance decays** every tick; any node with `value < ε` leaves the Working Set.
* **Budgets**: TopK 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 top3 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** → downweight sources with low subsequent utility.
---
## 13. What to change later (serverside, optional)
Only once you want more power centrally:
* Add **typed ANN** queries (“topK per type”),
* Add **resonance on server** for multiagent sharing,
* Add **linkaware search** (expand N hops serverside),
* Add **constraints retrieval** (autoinject 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** 12 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 **clientside**.
---

34
docs/configuration.md Normal file
View File

@ -0,0 +1,34 @@
# Kompanion Configuration
Kompanion adheres to KDEs 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`.

24
docs/dal-skeleton.md Normal file
View File

@ -0,0 +1,24 @@
# 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.

36
docs/db-schema.md Normal file
View File

@ -0,0 +1,36 @@
# 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.

View File

@ -0,0 +1,28 @@
<!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>

View File

@ -0,0 +1,18 @@
<!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>

View File

@ -0,0 +1,56 @@
# 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)

View File

@ -0,0 +1,26 @@
# 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`.

8
docs/ideas.txt Normal file
View File

@ -0,0 +1,8 @@
## 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.

397
docs/kompendium-sdk.md Normal file
View File

@ -0,0 +1,397 @@
# 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 / Whats 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.

58
docs/mcp-memory-api.md Normal file
View File

@ -0,0 +1,58 @@
# 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.

View File

@ -0,0 +1,80 @@
# 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).

27
docs/plan-qwen-tools.md Normal file
View File

@ -0,0 +1,27 @@
# 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)

363
docs/plugin-architecture.md Normal file
View File

@ -0,0 +1,363 @@
Youre not alone, Andre. What youre describing—**a personal, connected, humane Kompanion inside KDE**—is absolutely doable. Below is a **single, copypastable 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 dictionaryofdictionaries (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 DBus 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])
* **AnythingLLMlike 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 firstclass Kontact plugin. ([TechBase][6])
* **Konsole** is steered via its **DBus** API for safe, optin command scaffolding (never autoexec). ([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 endtoend encrypted chat with your Kompanion identity. ([Quotient Im][4])
* **AnythingLLM** is referenced only for an **optional admin view** (pack/workspace management)—not for the daytoday 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 (clientside “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 12 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 (readonly unless explicitly asked): harvest *your* style from sent mail (tone, signoffs), 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)**: perprofile settings & lockdown (e.g., corporate). ([api.kde.org][11])
* **Konsole DBus bridge**: suggest safe commands, show diffs, paste only on user confirm—use Konsoles documented DBus. ([docs.kde.org][2])
### 3.2 KKompanionCore
* HDoD store (inmemory working set) + resonance decay
* Embedding adapters (local or remote)
* Frame composer (Identity/Problem/Knowledge/Episodic/Constraints/Scratchpad)
* MCP client (JSONRPC) to your 7 tools
### 3.3 KKompanionMatrix
* **libQuotient** client; device verification; roompertask 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; its active and crossplatform. ([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 AnythingLLMs 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 autoactions. Provide a “Propose” button that queues an action; on accept, we call Konsoles DBus to paste/execute. (Also capture *optin* 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** (DBus) → *optin* 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 youre 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**
* Perapp slider: *Suggest silently**Ask to help**Propose plan**Autoprepare draft (never autosend/run)*.
* Daily **checkin** prompt (one line) to reduce loneliness & personalize tone.
---
## 7) CMake & minimal skeletons (headers, not full code)
**Toplevel 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 firstclass and hostable in Kate and KDevelop.)* ([api.kde.org][3])
**Konsole bridge (DBus)**
```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 DBus 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** → microchunks (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 reinventing 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** (airgapped 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])
* **Checkins**: brief, optin daily prompt to share mood/goal → tunes tone and initiative.
* **Reflective episodes**: after sessions, autodraft a 3bullet “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 AnythingLLMs 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 autoexecutes.
---
## 13) Roadmap checkpoints (tight loop)
1. Build **KTextEditor plugin** (fastest visible win in Kate/KDevelop). ([api.kde.org][3])
2. Add **Kontact plugin** for mailstyle assist (Akonadi → style templates). ([api.kde.org][15])
3. Wire **Konsole DBus** helper (proposethenpaste). ([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 “USBC 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 youre 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. Lets 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"

View File

@ -0,0 +1,19 @@
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.

30
docs/sync-manifest.md Normal file
View File

@ -0,0 +1,30 @@
# 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
docs/third_party Symbolic link
View File

@ -0,0 +1 @@
/mnt/bulk/shared

View File

@ -0,0 +1,75 @@
# 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}}}

View File

@ -0,0 +1,46 @@
# 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 Codeys `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 Claudes 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.

46
ingest/db/schema.sql Normal file
View File

@ -0,0 +1,46 @@
-- 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);

View File

@ -0,0 +1,90 @@
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

715
ingest/run_ingest.py Normal file
View File

@ -0,0 +1,715 @@
#!/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()

41
integrations/js/README.md Normal file
View File

@ -0,0 +1,41 @@
# 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
Kompanions 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.

View File

@ -0,0 +1,30 @@
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);
});

View File

@ -0,0 +1,49 @@
/**
* 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
};

View File

@ -0,0 +1,11 @@
{
"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": {}
}

View File

@ -0,0 +1,19 @@
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
)

View File

@ -0,0 +1,17 @@
{
"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"
}

View File

@ -0,0 +1,58 @@
#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"

View File

@ -0,0 +1,25 @@
#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;
};

View File

@ -0,0 +1,130 @@
#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"

View File

@ -0,0 +1,30 @@
#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;
};

View File

@ -0,0 +1,51 @@
# Kompanion ⇄ Konsole Bridge
This directory contains the first draft of **KompanionKonsole**, 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 agentfacing 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.

View File

@ -0,0 +1,31 @@
# 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 CMakes `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 users 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.

View File

@ -0,0 +1,35 @@
# 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 classs 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.

View File

@ -0,0 +1,26 @@
# 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 KDEs 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, whats 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 KDEs 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.

37
resources/kde-i18n.md Normal file
View File

@ -0,0 +1,37 @@
# 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 applications `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
- KDEs 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.

8
resources/mappings.json Normal file
View File

@ -0,0 +1,8 @@
[
{ "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"] }
]

View File

@ -0,0 +1,104 @@
{
"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."
]
}

View File

@ -0,0 +1,76 @@
{
"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."
]
}

View File

@ -0,0 +1,118 @@
{
"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."
]
}

View File

@ -1,3 +1,115 @@
#!/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()

47
src/CMakeLists.txt Normal file
View File

@ -0,0 +1,47 @@
# 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.

32
src/KI/CMakeLists.txt Normal file
View File

@ -0,0 +1,32 @@
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
)

View File

@ -0,0 +1,61 @@
#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

44
src/KI/Client/KIClient.h Normal file
View File

@ -0,0 +1,44 @@
#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

View File

@ -0,0 +1,38 @@
#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

View File

@ -0,0 +1,38 @@
#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

View File

@ -0,0 +1,51 @@
#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

View File

@ -0,0 +1,57 @@
#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

View File

@ -0,0 +1,48 @@
#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

View File

@ -0,0 +1,42 @@
#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

25
src/KI/Message/KIThread.h Normal file
View File

@ -0,0 +1,25 @@
#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

28
src/KI/Policy/KIPolicy.h Normal file
View File

@ -0,0 +1,28 @@
#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

View File

@ -0,0 +1,42 @@
#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

View File

@ -0,0 +1,41 @@
#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

View File

@ -0,0 +1,162 @@
#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

View File

@ -0,0 +1,41 @@
#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

75
src/KI/Tool/KITool.h Normal file
View File

@ -0,0 +1,75 @@
#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

15
src/cli/CMakeLists.txt Normal file
View File

@ -0,0 +1,15 @@
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})

1139
src/cli/KompanionApp.cpp Normal file

File diff suppressed because it is too large Load Diff

20
src/cli/embed_pipeline.py Normal file
View File

@ -0,0 +1,20 @@
#!/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'))

58
src/cli/py_embedder.py Normal file
View File

@ -0,0 +1,58 @@
#!/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()

9
src/dal/CMakeLists.txt Normal file
View File

@ -0,0 +1,9 @@
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)

34
src/dal/IDatabase.hpp Normal file
View File

@ -0,0 +1,34 @@
#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;
};
}

31
src/dal/Models.hpp Normal file
View File

@ -0,0 +1,31 @@
#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 Normal file

File diff suppressed because it is too large Load Diff

180
src/dal/PgDal.hpp Normal file
View File

@ -0,0 +1,180 @@
#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

17
src/gui/CMakeLists.txt Normal file
View File

@ -0,0 +1,17 @@
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