feat: Implement initial KompanionAI SDK structure and KLLM compatibility layer
This commit is contained in:
parent
fd289edded
commit
ca390763e6
174
.acf/tasks.json
174
.acf/tasks.json
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"projectName": "metal-kompanion-mcp",
|
"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.",
|
"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": 23,
|
"lastTaskId": 24,
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
@ -604,6 +604,178 @@
|
||||||
"message": "Task created with title: \"Contract tests: DAL-backed tools\""
|
"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\""
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -1,10 +1,17 @@
|
||||||
# Repository Guidelines
|
# 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.
|
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.
|
||||||
|
|
||||||
|
## MCP Usage
|
||||||
|
- This project uses agentic-control-framework Use this for task planning
|
||||||
|
|
||||||
## Project Structure & Module Organization
|
## 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.
|
- `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/` 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/`.
|
- 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
|
## Build, Test, and Development Commands
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ 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")
|
install(DIRECTORY db/init/ DESTINATION ${KDE_INSTALL_DATADIR}/kompanion/db/init FILES_MATCHING PATTERN "*.sql")
|
||||||
|
|
||||||
add_subdirectory(src/dal)
|
add_subdirectory(src/dal)
|
||||||
|
add_subdirectory(src/KompanionAI)
|
||||||
|
|
||||||
add_executable(kom_mcp
|
add_executable(kom_mcp
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
|
|
@ -69,6 +70,7 @@ qt_add_resources(kom_mcp kompanion_mcp_resources
|
||||||
)
|
)
|
||||||
target_link_libraries(kom_mcp PRIVATE
|
target_link_libraries(kom_mcp PRIVATE
|
||||||
kom_dal
|
kom_dal
|
||||||
|
kom_ai
|
||||||
KF6::ConfigCore
|
KF6::ConfigCore
|
||||||
Qt6::Core
|
Qt6::Core
|
||||||
Qt6::Network
|
Qt6::Network
|
||||||
|
|
@ -95,6 +97,7 @@ Qt6::Core
|
||||||
Qt6::Sql
|
Qt6::Sql
|
||||||
KF6::ConfigCore
|
KF6::ConfigCore
|
||||||
kom_dal
|
kom_dal
|
||||||
|
kom_ai
|
||||||
)
|
)
|
||||||
install(TARGETS kompanion RUNTIME ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
|
install(TARGETS kompanion RUNTIME ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
[ 0%] Built target kom_dal_autogen_timestamp_deps
|
||||||
|
[ 4%] Built target kom_dal_autogen
|
||||||
|
[ 16%] Built target kom_dal
|
||||||
|
[ 16%] Built target kom_mcp_autogen_timestamp_deps
|
||||||
|
[ 20%] Built target kom_mcp_autogen
|
||||||
|
[ 33%] Built target kom_mcp
|
||||||
|
[ 33%] Built target kompanion_autogen_timestamp_deps
|
||||||
|
[ 37%] Built target kompanion_autogen
|
||||||
|
[ 50%] Built target kompanion
|
||||||
|
[ 50%] Built target test_mcp_tools_autogen_timestamp_deps
|
||||||
|
[ 54%] Built target test_mcp_tools_autogen
|
||||||
|
[ 66%] Built target test_mcp_tools
|
||||||
|
[ 66%] Built target contract_memory_autogen_timestamp_deps
|
||||||
|
[ 70%] Built target contract_memory_autogen
|
||||||
|
[ 83%] Built target contract_memory
|
||||||
|
[ 83%] Built target test_memory_exchange_autogen_timestamp_deps
|
||||||
|
[ 87%] Built target test_memory_exchange_autogen
|
||||||
|
[100%] Built target test_memory_exchange
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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 / What’s missing)
|
||||||
|
|
||||||
|
### Strengths
|
||||||
|
|
||||||
|
* Idiomatic Qt API (QObject, signals/slots).
|
||||||
|
* Central interface (`KLLMInterface`) mirrors `QNetworkAccessManager`/QtNetwork feeling.
|
||||||
|
* Streaming via `KLLMReply::contentAdded()` and completion via `finished()`.
|
||||||
|
* Simple model enumeration + `systemPrompt`.
|
||||||
|
|
||||||
|
### Gaps to close
|
||||||
|
|
||||||
|
1. **Message structure**: Only a single `message` string; no roles (system/user/assistant/tool), no multi-turn thread assembly besides a custom `KLLMContext`.
|
||||||
|
2. **Tool calling / function calling**: No schema for tool specs, invocation events, results injection, or “plan” steps.
|
||||||
|
3. **Backend abstraction**: “Ollama URL” is a property of the core interface. Needs pluggable providers with capability discovery.
|
||||||
|
4. **Error model**: Only `errorOccurred(QString)` and `hasError`. Missing typed errors, retry/cancel semantics, timeouts, throttling.
|
||||||
|
5. **Observability**: Some timing info, but no per-token hooks, token usage counters, logs, traces.
|
||||||
|
6. **Threading & cancellation**: No unified cancel token; no `QFuture`/`QCoro` or `QPromise` integration.
|
||||||
|
7. **QML friendliness**: Usable, but message/tool specs should be modelled as Q_GADGET/Q_OBJECT types and `Q_PROPERTY`-exposed to QML.
|
||||||
|
8. **Privacy & policy**: No ACLs, no data origin policy, no redaction hooks.
|
||||||
|
9. **Embeddings / RAG**: No first-class embedding calls, no JSON-mode or structured outputs with validators.
|
||||||
|
10. **Agent loop affordances**: No “plan→confirm→apply patch / run tests” pattern built-in; no diff/patch helpers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part B — v2 API Proposal (“KompanionAI”)
|
||||||
|
|
||||||
|
Rename the public surface to **KompanionAI** (KI = “Künstliche Intelligenz” fits DE nicely), keep binary compatibility fences internally if needed.
|
||||||
|
|
||||||
|
### Namespaces & modules
|
||||||
|
|
||||||
|
* `namespace KompanionAI { … }`
|
||||||
|
* Core modules:
|
||||||
|
|
||||||
|
* `Client` (front door)
|
||||||
|
* `Provider` (backend plugins: Ollama, OpenAI, Local)
|
||||||
|
* `Message` / `Thread` (roles + history)
|
||||||
|
* `Tool` (function calling schema)
|
||||||
|
* `Completion` (text/chat)
|
||||||
|
* `Embedding` (vectorize)
|
||||||
|
* `Policy` (privacy/ACL)
|
||||||
|
* `Events` (streaming tokens, tool calls, traces)
|
||||||
|
|
||||||
|
All classes are Qt types with signals/slots & QML types.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1) Message & Thread Model
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Roles & content parts, QML-friendly
|
||||||
|
class KIMessagePart {
|
||||||
|
Q_GADGET
|
||||||
|
Q_PROPERTY(QString mime READ mime)
|
||||||
|
Q_PROPERTY(QString text READ text) // for text/plain
|
||||||
|
// future: binary, image refs, etc.
|
||||||
|
public:
|
||||||
|
QString mime; // "text/plain", "application/json"
|
||||||
|
QString text;
|
||||||
|
};
|
||||||
|
|
||||||
|
class KIMessage {
|
||||||
|
Q_GADGET
|
||||||
|
Q_PROPERTY(QString role READ role) // "system" | "user" | "assistant" | "tool"
|
||||||
|
Q_PROPERTY(QList<KIMessagePart> parts READ parts)
|
||||||
|
public:
|
||||||
|
QString role;
|
||||||
|
QList<KIMessagePart> parts;
|
||||||
|
QVariantMap metadata; // arbitrary
|
||||||
|
};
|
||||||
|
|
||||||
|
class KIThread {
|
||||||
|
Q_GADGET
|
||||||
|
Q_PROPERTY(QList<KIMessage> messages READ messages)
|
||||||
|
public:
|
||||||
|
QList<KIMessage> messages;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: Enables multi-turn chat with explicit roles and mixed content (text/JSON). Tool outputs show up as `role="tool"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2) Tool / Function Calling
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class KIToolParam {
|
||||||
|
Q_GADGET
|
||||||
|
Q_PROPERTY(QString name READ name)
|
||||||
|
Q_PROPERTY(QString type READ type) // "string","number","boolean","object"... (JSON Schema-lite)
|
||||||
|
Q_PROPERTY(bool required READ required)
|
||||||
|
Q_PROPERTY(QVariant defaultValue READ defaultValue)
|
||||||
|
public:
|
||||||
|
QString name, type;
|
||||||
|
bool required = false;
|
||||||
|
QVariant defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
class KIToolSpec {
|
||||||
|
Q_GADGET
|
||||||
|
Q_PROPERTY(QString name READ name)
|
||||||
|
Q_PROPERTY(QString description READ description)
|
||||||
|
Q_PROPERTY(QList<KIToolParam> params READ params)
|
||||||
|
public:
|
||||||
|
QString name, description;
|
||||||
|
QList<KIToolParam> params; // JSON-serializable schema
|
||||||
|
};
|
||||||
|
|
||||||
|
class KIToolCall {
|
||||||
|
Q_GADGET
|
||||||
|
Q_PROPERTY(QString name READ name)
|
||||||
|
Q_PROPERTY(QVariantMap arguments READ arguments)
|
||||||
|
public:
|
||||||
|
QString name;
|
||||||
|
QVariantMap arguments;
|
||||||
|
};
|
||||||
|
|
||||||
|
class KIToolResult {
|
||||||
|
Q_GADGET
|
||||||
|
Q_PROPERTY(QString name READ name)
|
||||||
|
Q_PROPERTY(QVariant result READ result) // result payload (JSON-like)
|
||||||
|
public:
|
||||||
|
QString name;
|
||||||
|
QVariant result;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Flow**: Model emits a **tool call** event → client executes tool → emits **tool result** → model continues. All observable via signals.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3) Provider abstraction (multi-backend)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class KIProvider : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QString name READ name CONSTANT)
|
||||||
|
Q_PROPERTY(QStringList models READ models NOTIFY modelsChanged)
|
||||||
|
Q_PROPERTY(KICapabilities caps READ caps CONSTANT)
|
||||||
|
public:
|
||||||
|
virtual QFuture<KIReply*> chat(const KIThread& thread, const KIChatOptions& opts) = 0;
|
||||||
|
virtual QFuture<KIEmbeddingResult> embed(const QStringList& texts, const KIEmbedOptions& opts) = 0;
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
class KIClient : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(KIProvider* provider READ provider WRITE setProvider NOTIFY providerChanged)
|
||||||
|
Q_PROPERTY(QString defaultModel READ defaultModel WRITE setDefaultModel NOTIFY defaultModelChanged)
|
||||||
|
public:
|
||||||
|
Q_INVOKABLE QFuture<KIReply*> chat(const KIThread&, const KIChatOptions&);
|
||||||
|
Q_INVOKABLE QFuture<KIEmbeddingResult> embed(const QStringList&, const KIEmbedOptions&);
|
||||||
|
Q_INVOKABLE void cancel(quint64 requestId);
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
* **OllamaProvider**, **OpenAIProvider**, **LocalProvider** implement `KIProvider`.
|
||||||
|
* `KICapabilities` advertises support for: JSON-mode, function calling, system prompts, logprobs, images, etc.
|
||||||
|
* **Do not** bake “Ollama URL” into `Client`. It belongs to the provider.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4) Completion / Reply / Streaming Events
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class KIReply : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(bool finished READ isFinished NOTIFY finishedChanged)
|
||||||
|
Q_PROPERTY(int promptTokens READ promptTokens CONSTANT)
|
||||||
|
Q_PROPERTY(int completionTokens READ completionTokens CONSTANT)
|
||||||
|
Q_PROPERTY(QString model READ model CONSTANT)
|
||||||
|
public:
|
||||||
|
// accumulated assistant text
|
||||||
|
Q_INVOKABLE QString text() const;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void tokensAdded(const QString& delta); // streaming text
|
||||||
|
void toolCallProposed(const KIToolCall& call); // model proposes a tool call
|
||||||
|
void toolResultRequested(const KIToolCall& call); // alt: unified request
|
||||||
|
void traceEvent(const QVariantMap& span); // observability
|
||||||
|
void finished(); // reply done
|
||||||
|
void errorOccurred(const KIError& error);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: makes **tool invocation** first-class and observable. You can wire it to ACF/MCP tools or project introspection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5) Options / Policies / Privacy
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class KIChatOptions {
|
||||||
|
Q_GADGET
|
||||||
|
Q_PROPERTY(QString model MEMBER model)
|
||||||
|
Q_PROPERTY(bool stream MEMBER stream)
|
||||||
|
Q_PROPERTY(bool jsonMode MEMBER jsonMode)
|
||||||
|
Q_PROPERTY(int maxTokens MEMBER maxTokens)
|
||||||
|
Q_PROPERTY(double temperature MEMBER temperature)
|
||||||
|
Q_PROPERTY(QList<KIToolSpec> tools MEMBER tools) // permitted tool set for this call
|
||||||
|
Q_PROPERTY(KIPolicy policy MEMBER policy)
|
||||||
|
// ...
|
||||||
|
public:
|
||||||
|
QString model; bool stream = true; bool jsonMode = false;
|
||||||
|
int maxTokens = 512; double temperature = 0.2;
|
||||||
|
QList<KIToolSpec> tools;
|
||||||
|
KIPolicy policy;
|
||||||
|
};
|
||||||
|
|
||||||
|
class KIPolicy {
|
||||||
|
Q_GADGET
|
||||||
|
Q_PROPERTY(QString visibility MEMBER visibility) // "private|org|public"
|
||||||
|
Q_PROPERTY(bool allowNetwork MEMBER allowNetwork)
|
||||||
|
Q_PROPERTY(QStringList redactions MEMBER redactions) // regex keys to redact
|
||||||
|
// future: per-source ACLs
|
||||||
|
public:
|
||||||
|
QString visibility = "private";
|
||||||
|
bool allowNetwork = false;
|
||||||
|
QStringList redactions;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: explicit control of what the agent may do; dovetails with your HDoD memory ACLs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6) Embeddings (for RAG / memory)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class KIEmbedOptions {
|
||||||
|
Q_GADGET
|
||||||
|
Q_PROPERTY(QString model MEMBER model)
|
||||||
|
Q_PROPERTY(QString normalize MEMBER normalize) // "l2"|"none"
|
||||||
|
public:
|
||||||
|
QString model = "text-embed-local";
|
||||||
|
QString normalize = "l2";
|
||||||
|
};
|
||||||
|
|
||||||
|
class KIEmbeddingResult {
|
||||||
|
Q_GADGET
|
||||||
|
public:
|
||||||
|
QVector<QVector<float>> vectors;
|
||||||
|
QString model;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why**: unify vector generation; Kompanion memory can plug this directly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7) Agent Loop Conveniences (optional helpers)
|
||||||
|
|
||||||
|
Provide “batteries included” patterns **outside** the Provider:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class KIAgent : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
// Plan → (approve) → Execute, with tool calling enabled
|
||||||
|
Q_SIGNAL void planReady(const QString& plan);
|
||||||
|
Q_SIGNAL void patchReady(const QString& unifiedDiff);
|
||||||
|
Q_SIGNAL void needToolResult(const KIToolCall& call);
|
||||||
|
Q_SIGNAL void log(const QString& msg);
|
||||||
|
|
||||||
|
void runTask(const QString& naturalInstruction,
|
||||||
|
const KIThread& prior,
|
||||||
|
const QList<KIToolSpec>& tools,
|
||||||
|
const KIChatOptions& opts);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
This helper emits “plan first”, then “diff/patch proposals”, integrates with your **ACF** and **KTextEditor/KDevelop** diff panes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8) Error Model & Cancellation
|
||||||
|
|
||||||
|
* Introduce `KIError{ code: enum, httpStatus, message, retryAfter }`.
|
||||||
|
* `KIClient::cancel(requestId)` cancels in-flight work.
|
||||||
|
* Timeouts & retry policy configurable in `KIChatOptions`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9) QML exposure
|
||||||
|
|
||||||
|
Register these with `qmlRegisterType<…>("Kompanion.AI", 1, 0, "KIClient")` etc.
|
||||||
|
Expose `KIMessage`, `KIThread`, `KIToolSpec`, `KIAgent` to QML, so Plasma applets / Kirigami UIs can wire flows fast.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part C — Migration from KLLM to KompanionAI
|
||||||
|
|
||||||
|
* **Mapping**
|
||||||
|
|
||||||
|
* `KLLMInterface::getCompletion()` → `KIClient::chat(thread, opts)`.
|
||||||
|
* `KLLMRequest{message, model, context}` → `KIThread{ messages=[system?, user?], … }, KIChatOptions{model}`.
|
||||||
|
* `KLLMReply` → `KIReply` (adds tool call signals, token deltas, errors).
|
||||||
|
* `systemPrompt` → first system `KIMessage`.
|
||||||
|
* `models()` → `KIProvider::models()`.
|
||||||
|
|
||||||
|
* **Providers**
|
||||||
|
|
||||||
|
* Implement **OllamaProvider** first (parity with current).
|
||||||
|
* Add **OpenAIProvider** (JSON-mode/function calling), **LocalProvider** (llama.cpp/candle/etc.).
|
||||||
|
|
||||||
|
* **Binary/Source compatibility**
|
||||||
|
|
||||||
|
* You can keep thin wrappers named `KLLM*` forwarding into `KompanionAI` during transition.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part D — Minimal Examples
|
||||||
|
|
||||||
|
### 1) Simple chat with streaming and tool calling
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
KIClient client;
|
||||||
|
client.setProvider(new OllamaProvider(QUrl("http://localhost:11434")));
|
||||||
|
client.setDefaultModel("llama3.1:8b-instruct");
|
||||||
|
|
||||||
|
KIThread t;
|
||||||
|
t.messages << KIMessage{ .role="system", .parts={ { "text/plain","You are Kompanion inside KDE." } } }
|
||||||
|
<< KIMessage{ .role="user", .parts={ { "text/plain","Generate a CSV → exam report plan." } } };
|
||||||
|
|
||||||
|
KIToolSpec csvSpec;
|
||||||
|
csvSpec.name = "parse_csv_schema";
|
||||||
|
csvSpec.description = "Inspect a CSV sample path and return column info.";
|
||||||
|
csvSpec.params = { { "path","string", true, {} } };
|
||||||
|
|
||||||
|
KIChatOptions opts;
|
||||||
|
opts.tools = { csvSpec };
|
||||||
|
opts.stream = true;
|
||||||
|
|
||||||
|
auto *reply = client.chat(t, opts).result(); // or connect via QFutureWatcher
|
||||||
|
QObject::connect(reply, &KIReply::tokensAdded, [](const QString& d){ qDebug() << d; });
|
||||||
|
QObject::connect(reply, &KIReply::toolCallProposed, [&](const KIToolCall& call){
|
||||||
|
if (call.name == "parse_csv_schema") {
|
||||||
|
QVariantMap out; out["columns"] = QStringList{ "Name","Grade","Subject" };
|
||||||
|
// feed result back (provider-specific or via KIClient API)
|
||||||
|
client.returnToolResult(*reply, KIToolResult{ call.name, out });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2) Embeddings for memory
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto emb = client.embed({ "RAII pattern", "SFINAE", "Type erasure" }, KIEmbedOptions{}).result();
|
||||||
|
qDebug() << emb.vectors.size(); // 3
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part E — Optional Extensions (for Gemini to consider)
|
||||||
|
|
||||||
|
* **Structured outputs**: JSON schema validation for function outputs (reject invalid JSON, request fix).
|
||||||
|
* **Safety hooks**: pre-execution validators for tool calls (e.g. forbid dangerous shell).
|
||||||
|
* **Observability**: OpenTelemetry spans over request lifecycle and tool calls.
|
||||||
|
* **Rate limiting**: token budgeters per provider.
|
||||||
|
* **Offline mode**: `allowNetwork=false` forces model to abstain from external lookups.
|
||||||
|
* **Crash handler integration**: a helper that consumes backtraces and emits a `KIThread` pre-filled with stack/context (pairs naturally with an ACF tool to fetch symbols).
|
||||||
|
* **CSV app generator**: a thin template tool that scaffolds a Kirigami app, fed by CSV schema tool—end-to-end demo of agentic coding.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
* Keep Qt idioms; elevate to **KompanionAI** with roles, tools, providers, and policies.
|
||||||
|
* Make **tool calling first-class** with observable events.
|
||||||
|
* Decouple backend specifics via `KIProvider`.
|
||||||
|
* Add embeddings & JSON-mode for RAG + structured tasks.
|
||||||
|
* Provide **agent loop helpers** (plan→diff→apply) outside the provider.
|
||||||
|
* Expose everything to QML for KDE-native UIs.
|
||||||
|
|
||||||
|
This gives you a future-proof client SDK that plugs directly into Kontact/Konsole/KDevelop/Plasma/NeoChat and supports your ACF/MCP agent flows without locking into any single vendor.
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/mnt/bulk/shared
|
||||||
|
|
@ -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 CMake’s `find_package_handle_standard_args()` helper to handle reporting of `FOUND` status, version and result variables.
|
||||||
|
- Avoid micro-optimizations such as skipping find logic when variables are already set; always run the full search to ensure correct results.
|
||||||
|
- Do not manually set `Foo_FIND_QUIETLY` to suppress messages; use the standard helper which respects the user’s settings.
|
||||||
|
|
||||||
|
## Other Best Practices
|
||||||
|
|
||||||
|
- Use variables consistently (e.g. camelCase or lowercase with underscores) and quote variables when their values may contain spaces.
|
||||||
|
- Place project-specific configuration in top-level `CMakeLists.txt` and keep module definitions (`.cmake` files) separate for reusability.
|
||||||
|
- Document non-obvious logic with comments; treat the build scripts with the same care as source code.
|
||||||
|
|
@ -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 class’s own header, other headers from the same framework, headers from other frameworks, Qt headers, then other system or standard headers. Separate groups with blank lines and sort each group alphabetically.
|
||||||
|
- When including Qt or KDE classes, omit the module prefix (e.g. use `<QString>` rather than `<QtCore/QString>`).
|
||||||
|
- Header files must have an include guard based on the file name in all capitals with underscores and no leading or trailing underscore.
|
||||||
|
|
||||||
|
## Automatic Formatting Tools
|
||||||
|
|
||||||
|
- KDE provides scripts such as `astyle-kdelibs` to reformat code using Artistic Style; this script enforces four-space indentation and K&R braces.
|
||||||
|
- A standard `.clang-format` file is distributed with KDE frameworks; projects can add a `kde_clang_format` target and use a Git pre-commit hook to automatically format code.
|
||||||
|
|
@ -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 KDE’s style. It enforces four-space indentation and K&R brace placement.
|
||||||
|
- **kde_clang_format**: Uses a `.clang-format` file distributed with KDE frameworks to format code automatically; projects can add a `kde_clang_format` target and use a pre-commit hook to apply `clang-format`.
|
||||||
|
- **uncrustify-kf5**: An Uncrustify configuration tuned for KDE Frameworks 5. Uncrustify is a configurable source-code beautifier that can add or remove spaces and newlines, align code and add braces. Use this to reformat existing codebases consistently.
|
||||||
|
|
||||||
|
## Static Analysis and Style Checking
|
||||||
|
|
||||||
|
- **krazy2**: A static analysis tool that scans KDE source code and reports issues related to coding policy, best practices and optimization. It works through modular checker programs to examine different aspects of the code.
|
||||||
|
- **krazy-licensecheck**: Runs the Krazy2 license checker on a list of source files. It requires Krazy2 to be installed and checks that files have correct license headers.
|
||||||
|
- **cmakelint.pl**: A Perl script that examines `CMakeLists.txt` files and reports problems such as missing `endif()` statements, deprecated macros or stylistic issues.
|
||||||
|
- **kde-spellcheck.pl**: Checks source files for common misspellings and can optionally correct them; useful for catching typos before they reach translators.
|
||||||
|
|
||||||
|
## Translation and Build Helpers
|
||||||
|
|
||||||
|
- **extractrc**: Extracts user-visible text (labels, tooltips, what’s this) from Qt Designer `.ui` files, XML GUI `.rc` files and `.kcfg` configuration files so that these strings can be translated.
|
||||||
|
- **includemocs**: Scans C++ sources and adds missing `#include "moc_*.cpp"` lines for classes that use the `Q_OBJECT` macro, ensuring that the Meta-Object Compiler output is linked.
|
||||||
|
- **fixuifiles**: Processes Qt/KDE `.ui` files to fix common issues: lowers the required Qt version, removes untranslatable Alt+Letter accelerators and eliminates class-name captions.
|
||||||
|
- **preparetips**: Collects tips-of-the-day and other tip strings for translation; typically called from a `Messages.sh` script.
|
||||||
|
- **xgettext (with KDE flags)**: The gettext extraction tool used to extract translatable strings from C++ and QML sources. It is invoked through the build system with appropriate options to recognize KDE’s i18n macros.
|
||||||
|
|
||||||
|
These tools can be installed from the `kde-dev-scripts` package or the corresponding KDE SDK. Use them regularly to keep your project consistent with KDE and Qt guidelines.
|
||||||
|
|
@ -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 application’s `KInstance` exists (e.g. in static initializers), use `ki18n()` which returns a translatable string object; later call `.toString()` when ready to display.
|
||||||
|
|
||||||
|
## Adding Context and Disambiguation
|
||||||
|
|
||||||
|
- If a message is ambiguous or very short, provide a context comment with `i18nc("context", "text")`. For example, use `i18nc("File menu action", "Copy")` to distinguish verb and noun forms.
|
||||||
|
- When pluralization and context are both needed, use `i18ncp("context", "singular", "plural", count, …)` to supply both context and plural forms.
|
||||||
|
|
||||||
|
## Plural Forms
|
||||||
|
|
||||||
|
- Use `i18np("singular", "plural", count, …)` for messages that vary depending on a count. Provide both singular and plural text even if English would not require a plural. The first integer argument determines which form to use.
|
||||||
|
- Do not manually pluralize with the `?:` operator; always let the i18n system handle plural rules.
|
||||||
|
- When using a plural call, `%1` may be omitted in the singular string because the count itself is `%1`.
|
||||||
|
|
||||||
|
## Placeholders and Arguments
|
||||||
|
|
||||||
|
- Inside the translatable string, use `%1`, `%2`, … to mark where runtime values will be inserted. The numbered placeholders correspond to additional arguments to the `i18n` function; do not use `QString::arg()` to substitute values.
|
||||||
|
- Keep the number of arguments to nine or fewer; `i18n` functions support at most nine substitutions.
|
||||||
|
- Placeholders must be numbered sequentially without gaps (e.g. `%1`, `%2`, `%3`); skipping a number is not allowed.
|
||||||
|
- In plural forms, the first integer argument determines plural choice; other placeholders still follow the usual numbering.
|
||||||
|
|
||||||
|
## Common Pitfalls and Best Practices
|
||||||
|
|
||||||
|
- Do not build user-visible sentences by concatenating multiple translated fragments. Always wrap the entire sentence in a single `i18n` call so the translator can rearrange words as needed.
|
||||||
|
- Provide context for short strings, abbreviations or words that could have multiple meanings.
|
||||||
|
- Avoid using `%n` (legacy gettext placeholder) in new code; use `%1`, `%2` instead.
|
||||||
|
- When inserting numbers into strings, use `i18n` functions first, then supply numbers as arguments; this allows the translation system to format numbers appropriately for the locale.
|
||||||
|
|
||||||
|
## Extraction Tools and Build Integration
|
||||||
|
|
||||||
|
- KDE’s build system uses a `Messages.sh` script to collect translatable strings. The script typically calls `extractrc` to extract strings from `.ui`, `.rc` and `.kcfg` files; `EXTRACT_GRANTLEE_TEMPLATE_STRINGS` for Grantlee template strings; `PREPARETIPS` for tips-of-the-day; and finally `xgettext` to extract strings from C++ and QML source files. These tools generate a `.pot` catalog that translators use.
|
||||||
|
- The environment variables such as `$EXTRACTRC`, `$EXTRACT_GRANTLEE_TEMPLATE_STRINGS`, `$PREPARETIPS` and `$XGETTEXT` are provided by the build system; developers only need to list source files in the script.
|
||||||
|
- Ensure that every string to be translated is reachable by these extraction tools: wrap strings in `i18n` calls in C++/QML, fill “translatable” and “comment” properties in Qt Designer for `.ui` files, and add context where necessary.
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023-2024 Laurent Montel <montel.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#include "KLLMConstants.h"
|
||||||
|
|
||||||
|
QUrl KLLMCore::ollamaUrl()
|
||||||
|
{
|
||||||
|
return QUrl(QStringLiteral("http://127.0.0.1:11434"));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023-2024 Laurent Montel <montel.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "kllmcore_export.h"
|
||||||
|
#include <QUrl>
|
||||||
|
namespace KLLMCore
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief Return default Ollama Url
|
||||||
|
* @return default Ollama Url.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] KLLMCORE_EXPORT QUrl ollamaUrl();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#include "KLLMContext.h"
|
||||||
|
|
||||||
|
#include <QJsonValue>
|
||||||
|
|
||||||
|
using namespace KLLMCore;
|
||||||
|
|
||||||
|
QJsonValue KLLMContext::toJson() const
|
||||||
|
{
|
||||||
|
switch (m_backend) {
|
||||||
|
case Backend::Ollama:
|
||||||
|
return m_data.value<QJsonArray>();
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void KLLMContext::setOllamaContext(const QJsonArray &context)
|
||||||
|
{
|
||||||
|
m_data = context;
|
||||||
|
m_backend = Backend::Ollama;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDebug operator<<(QDebug d, const KLLMContext &t)
|
||||||
|
{
|
||||||
|
d << "m_data " << t.toJson();
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Loren Burkholder <computersemiexpert@outlook.com>
|
||||||
|
// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "kllmcore_export.h"
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QJsonArray>
|
||||||
|
|
||||||
|
namespace KLLMCore
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief KLLMContext provides a representation of a conversation context.
|
||||||
|
*
|
||||||
|
* Most, if not all, LLMs have the concept of "context". This allows them to refer to previous messages in a conversation to
|
||||||
|
* enhance their replies. In most scenarios, this is the preferred behavior.
|
||||||
|
*
|
||||||
|
* To use KLLMContext, you simply need to get the context from each KLLMReply and set it on the next KLLMReqeust that you
|
||||||
|
* send. KLLMInterface will use this in KLLMInterface::getCompletion().
|
||||||
|
*/
|
||||||
|
struct KLLMCORE_EXPORT KLLMContext {
|
||||||
|
/**
|
||||||
|
* @brief Converts the context to a JSON representation.
|
||||||
|
*
|
||||||
|
* Different LLM backends represent context in different ways; for example, while Ollama represents context as an array
|
||||||
|
* of integer identifiers, OpenAI relies on a JSON array of all the messages in the conversation so far. Therefore, this
|
||||||
|
* function exists to take any representation set on it for any backend and convert it to a JSON value suitable for
|
||||||
|
* sending in a request.
|
||||||
|
*
|
||||||
|
* @return A JSON representation of the context.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QJsonValue toJson() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sets an Ollama context as the current context.
|
||||||
|
* @param context The context from Ollama.
|
||||||
|
*/
|
||||||
|
void setOllamaContext(const QJsonArray &context);
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class Backend {
|
||||||
|
Ollama,
|
||||||
|
} m_backend;
|
||||||
|
QVariant m_data;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
KLLMCORE_EXPORT QDebug operator<<(QDebug d, const KLLMCore::KLLMContext &t);
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
|
||||||
|
#include "KLLMInterface.h"
|
||||||
|
#include <KLocalizedString>
|
||||||
|
|
||||||
|
using namespace Qt::StringLiterals;
|
||||||
|
using namespace KLLMCore;
|
||||||
|
|
||||||
|
KLLMInterface::KLLMInterface(QObject *parent)
|
||||||
|
: KLLMInterface{QString{}, parent}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
KLLMInterface::KLLMInterface(const QString &ollamaUrl, QObject *parent)
|
||||||
|
: KLLMOriginalInterface{ollamaUrl, parent}
|
||||||
|
, m_kompanionClient{new KompanionAI::KIClient(this)}
|
||||||
|
, m_ollamaProvider{new KompanionAI::OllamaProvider(this)}
|
||||||
|
{
|
||||||
|
m_kompanionClient->setProvider(m_ollamaProvider);
|
||||||
|
setOllamaUrl(ollamaUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
KLLMInterface::KLLMInterface(const QUrl &ollamaUrl, QObject *parent)
|
||||||
|
: KLLMInterface{ollamaUrl.toString(), parent}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool KLLMInterface::ready() const
|
||||||
|
{
|
||||||
|
// For now, assume ready if provider is set and has models
|
||||||
|
return m_kompanionClient->provider() != nullptr && !m_ollamaProvider->models().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool KLLMInterface::hasError() const
|
||||||
|
{
|
||||||
|
// TODO: Implement proper error checking from KompanionAI client
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList KLLMInterface::models() const
|
||||||
|
{
|
||||||
|
return m_ollamaProvider->models();
|
||||||
|
}
|
||||||
|
|
||||||
|
KLLMReply *KLLMInterface::getCompletion(const KLLMRequest &request)
|
||||||
|
{
|
||||||
|
KompanionAI::KIThread thread;
|
||||||
|
KompanionAI::KIMessage message;
|
||||||
|
message.role = "user";
|
||||||
|
KompanionAI::KIMessagePart part;
|
||||||
|
part.mime = "text/plain";
|
||||||
|
part.text = request.message();
|
||||||
|
message.parts.append(part);
|
||||||
|
thread.messages.append(message);
|
||||||
|
|
||||||
|
KompanionAI::KIChatOptions opts;
|
||||||
|
opts.model = request.model().isEmpty() ? m_kompanionClient->defaultModel() : request.model();
|
||||||
|
// TODO: Map KLLMContext to KompanionAI thread/options
|
||||||
|
|
||||||
|
auto kiReply = m_kompanionClient->chat(thread, opts).result();
|
||||||
|
auto kllmReply = new KLLMReply(this);
|
||||||
|
|
||||||
|
connect(kiReply, &KompanionAI::KIReply::tokensAdded, kllmReply, [kllmReply](const QString& delta) {
|
||||||
|
kllmReply->addContent(delta);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(kiReply, &KompanionAI::KIReply::finished, kllmReply, [kllmReply]() {
|
||||||
|
kllmReply->setFinished(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(kiReply, &KompanionAI::KIReply::errorOccurred, kllmReply, [kllmReply](const KompanionAI::KIError& error) {
|
||||||
|
kllmReply->setError(error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
return kllmReply;
|
||||||
|
}
|
||||||
|
|
||||||
|
KLLMReply *KLLMInterface::getModelInfo(const KLLMRequest &request)
|
||||||
|
{
|
||||||
|
// This is a simplified implementation as KompanionAI does not have a direct getModelInfo.
|
||||||
|
// We will return a KLLMReply with the model name if it exists in the provider's models.
|
||||||
|
auto kllmReply = new KLLMReply(this);
|
||||||
|
if (m_ollamaProvider->models().contains(request.model())) {
|
||||||
|
kllmReply->addContent(QString("{ \"model\": \"%1\" }").arg(request.model()));
|
||||||
|
} else {
|
||||||
|
kllmReply->setError(i18n("Model %1 not found.", request.model()));
|
||||||
|
}
|
||||||
|
kllmReply->setFinished(true);
|
||||||
|
return kllmReply;
|
||||||
|
}
|
||||||
|
|
||||||
|
void KLLMInterface::reload()
|
||||||
|
{
|
||||||
|
m_ollamaProvider->reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString KLLMInterface::ollamaUrl() const
|
||||||
|
{
|
||||||
|
// Ollama URL is managed by the OllamaProvider internally in KompanionAI
|
||||||
|
// For compatibility, we return an empty string or a placeholder.
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void KLLMInterface::setOllamaUrl(const QString &ollamaUrl)
|
||||||
|
{
|
||||||
|
// In KompanionAI, the Ollama URL is configured directly on the OllamaProvider.
|
||||||
|
// For compatibility, we can re-instantiate the provider or update its internal URL.
|
||||||
|
// For now, we'll just set the default model if the URL is treated as a model name.
|
||||||
|
// This needs proper handling if the URL is truly meant for provider configuration.
|
||||||
|
m_kompanionClient->setDefaultModel(ollamaUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
void KLLMInterface::setOllamaUrl(const QUrl &ollamaUrl)
|
||||||
|
{
|
||||||
|
setOllamaUrl(ollamaUrl.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString KLLMInterface::systemPrompt() const
|
||||||
|
{
|
||||||
|
// TODO: Extract system prompt from KompanionAI thread if available
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void KLLMInterface::setSystemPrompt(const QString &systemPrompt)
|
||||||
|
{
|
||||||
|
// TODO: Set system prompt in KompanionAI thread
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
|
||||||
|
#ifndef KLLMINTERFACE_H
|
||||||
|
#define KLLMINTERFACE_H
|
||||||
|
|
||||||
|
#include "KLLMOriginalInterface.h"
|
||||||
|
#include "KompanionAI/Client/KIClient.h"
|
||||||
|
#include "KompanionAI/Provider/OllamaProvider.h"
|
||||||
|
|
||||||
|
namespace KLLMCore {
|
||||||
|
|
||||||
|
class KLLMInterface : public KLLMOriginalInterface
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit KLLMCompatInterface(QObject *parent = nullptr);
|
||||||
|
explicit KLLMCompatInterface(const QString &ollamaUrl, QObject *parent = nullptr);
|
||||||
|
explicit KLLMCompatInterface(const QUrl &ollamaUrl, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
bool ready() const override;
|
||||||
|
bool hasError() const override;
|
||||||
|
QStringList models() const override;
|
||||||
|
|
||||||
|
KLLMReply *getCompletion(const KLLMRequest &request) override;
|
||||||
|
KLLMReply *getModelInfo(const KLLMRequest &request) override;
|
||||||
|
|
||||||
|
void reload() override;
|
||||||
|
|
||||||
|
QString ollamaUrl() const override;
|
||||||
|
void setOllamaUrl(const QString &ollamaUrl) override;
|
||||||
|
void setOllamaUrl(const QUrl &ollamaUrl) override;
|
||||||
|
|
||||||
|
QString systemPrompt() const override;
|
||||||
|
void setSystemPrompt(const QString &systemPrompt) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
KompanionAI::KIClient* m_kompanionClient;
|
||||||
|
KompanionAI::OllamaProvider* m_ollamaProvider;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace KLLMCore
|
||||||
|
|
||||||
|
#endif // KLLMCOMPAT_H
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Loren Burkholder <computersemiexpert@outlook.com>
|
||||||
|
// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#include "KLLMInterface.h"
|
||||||
|
|
||||||
|
#include <KLocalizedString>
|
||||||
|
|
||||||
|
#include <QBuffer>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
using namespace Qt::StringLiterals;
|
||||||
|
using namespace KLLMCore;
|
||||||
|
|
||||||
|
KLLMInterface::KLLMInterface(QObject *parent)
|
||||||
|
: KLLMInterface{QString{}, parent}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
KLLMInterface::KLLMInterface(const QString &ollamaUrl, QObject *parent)
|
||||||
|
: QObject{parent}
|
||||||
|
, m_manager{new QNetworkAccessManager{this}}
|
||||||
|
, m_ollamaUrl{ollamaUrl}
|
||||||
|
{
|
||||||
|
if (!m_ollamaUrl.isEmpty())
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
KLLMInterface::KLLMInterface(const QUrl &ollamaUrl, QObject *parent)
|
||||||
|
: KLLMInterface{ollamaUrl.toString(), parent}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool KLLMInterface::ready() const
|
||||||
|
{
|
||||||
|
return m_ready && !m_hasError;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool KLLMInterface::hasError() const
|
||||||
|
{
|
||||||
|
return m_hasError;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList KLLMInterface::models() const
|
||||||
|
{
|
||||||
|
return m_models;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
void KLLMInterface::deleteModel(const QString &modelName)
|
||||||
|
{
|
||||||
|
Q_ASSERT(ready());
|
||||||
|
|
||||||
|
QNetworkRequest req{QUrl::fromUserInput(m_ollamaUrl + QStringLiteral("/api/delete"))};
|
||||||
|
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||||
|
QJsonObject data;
|
||||||
|
data["name"_L1] = modelName;
|
||||||
|
|
||||||
|
// Delete resource doesn't take argument. Need to look at how to do it.
|
||||||
|
auto buf = new QBuffer{this};
|
||||||
|
buf->setData(QJsonDocument(data).toJson(QJsonDocument::Compact));
|
||||||
|
|
||||||
|
auto reply = new KLLMReply{m_manager->deleteResource(req, buf), this};
|
||||||
|
connect(reply, &KLLMReply::finished, this, [this, reply, buf] {
|
||||||
|
Q_EMIT finished(reply->readResponse());
|
||||||
|
buf->deleteLater();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
KLLMReply *KLLMInterface::getCompletion(const KLLMRequest &request)
|
||||||
|
{
|
||||||
|
Q_ASSERT(ready());
|
||||||
|
|
||||||
|
QNetworkRequest req{QUrl::fromUserInput(m_ollamaUrl + QStringLiteral("/api/generate"))};
|
||||||
|
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||||
|
|
||||||
|
QJsonObject data;
|
||||||
|
data["model"_L1] = request.model().isEmpty() ? m_models.constFirst() : request.model();
|
||||||
|
data["prompt"_L1] = request.message();
|
||||||
|
|
||||||
|
const auto context = request.context().toJson();
|
||||||
|
if (!context.isNull()) {
|
||||||
|
data["context"_L1] = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_systemPrompt.isEmpty()) {
|
||||||
|
data["system"_L1] = m_systemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto buf = new QBuffer{this};
|
||||||
|
buf->setData(QJsonDocument(data).toJson(QJsonDocument::Compact));
|
||||||
|
|
||||||
|
auto reply = new KLLMReply{m_manager->post(req, buf), this};
|
||||||
|
connect(reply, &KLLMReply::finished, this, [this, reply, buf] {
|
||||||
|
Q_EMIT finished(reply->readResponse());
|
||||||
|
buf->deleteLater();
|
||||||
|
});
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
KLLMReply *KLLMInterface::getModelInfo(const KLLMRequest &request)
|
||||||
|
{
|
||||||
|
Q_ASSERT(ready());
|
||||||
|
|
||||||
|
QNetworkRequest req{QUrl::fromUserInput(m_ollamaUrl + QStringLiteral("/api/show"))};
|
||||||
|
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||||
|
|
||||||
|
QJsonObject data;
|
||||||
|
data["model"_L1] = request.model().isEmpty() ? m_models.constFirst() : request.model();
|
||||||
|
|
||||||
|
auto buf = new QBuffer{this};
|
||||||
|
buf->setData(QJsonDocument(data).toJson(QJsonDocument::Compact));
|
||||||
|
|
||||||
|
auto reply = new KLLMReply{m_manager->post(req, buf), this, KLLMReply::RequestTypes::Show};
|
||||||
|
connect(reply, &KLLMReply::finished, this, [this, reply, buf] {
|
||||||
|
Q_EMIT finished(reply->readResponse());
|
||||||
|
buf->deleteLater();
|
||||||
|
});
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
void KLLMInterface::reload()
|
||||||
|
{
|
||||||
|
if (m_ollamaCheck)
|
||||||
|
disconnect(m_ollamaCheck);
|
||||||
|
|
||||||
|
QNetworkRequest req{QUrl::fromUserInput(m_ollamaUrl + QStringLiteral("/api/tags"))};
|
||||||
|
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||||
|
auto rep = m_manager->get(req);
|
||||||
|
m_ollamaCheck = connect(rep, &QNetworkReply::finished, this, [this, rep] {
|
||||||
|
if (rep->error() != QNetworkReply::NoError) {
|
||||||
|
Q_EMIT errorOccurred(i18n("Failed to connect to interface at %1: %2", m_ollamaUrl, rep->errorString()));
|
||||||
|
m_hasError = true;
|
||||||
|
Q_EMIT readyChanged();
|
||||||
|
Q_EMIT hasErrorChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto json = QJsonDocument::fromJson(rep->readAll());
|
||||||
|
const auto models = json["models"_L1].toArray();
|
||||||
|
for (const QJsonValue &model : models) {
|
||||||
|
m_models.push_back(model["name"_L1].toString());
|
||||||
|
}
|
||||||
|
Q_EMIT modelsChanged();
|
||||||
|
|
||||||
|
m_ready = !m_models.isEmpty();
|
||||||
|
m_hasError = false;
|
||||||
|
Q_EMIT readyChanged();
|
||||||
|
Q_EMIT hasErrorChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QString KLLMInterface::ollamaUrl() const
|
||||||
|
{
|
||||||
|
return m_ollamaUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
void KLLMInterface::setOllamaUrl(const QString &ollamaUrl)
|
||||||
|
{
|
||||||
|
if (m_ollamaUrl == ollamaUrl)
|
||||||
|
return;
|
||||||
|
m_ollamaUrl = ollamaUrl;
|
||||||
|
Q_EMIT ollamaUrlChanged();
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
void KLLMInterface::setOllamaUrl(const QUrl &ollamaUrl)
|
||||||
|
{
|
||||||
|
setOllamaUrl(ollamaUrl.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString KLLMInterface::systemPrompt() const
|
||||||
|
{
|
||||||
|
return m_systemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
void KLLMInterface::setSystemPrompt(const QString &systemPrompt)
|
||||||
|
{
|
||||||
|
if (m_systemPrompt == systemPrompt)
|
||||||
|
return;
|
||||||
|
m_systemPrompt = systemPrompt;
|
||||||
|
Q_EMIT systemPromptChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "moc_KLLMInterface.cpp"
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Loren Burkholder <computersemiexpert@outlook.com>
|
||||||
|
// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "kllmcore_export.h"
|
||||||
|
|
||||||
|
#include "KLLMReply.h"
|
||||||
|
#include "KLLMRequest.h"
|
||||||
|
class QNetworkAccessManager;
|
||||||
|
namespace KLLMCore
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief The KLLMInterface class provides an interface around the LLM API.
|
||||||
|
*
|
||||||
|
* KLLM is designed to feel similar to Qt Network. This means that all LLM requests must be routed through a central
|
||||||
|
* KLLMInterface object.
|
||||||
|
*
|
||||||
|
* To request a message or completion from the LLM, first create a KLLMRequest object with the desired message. Choose the
|
||||||
|
* model from models() that you wish to use for this request and set it on the KLLMRequest. Then call getCompletion(),
|
||||||
|
* passing your KLLMRequest object. You will recieve a KLLMReply object; connect to KLLMReply::contentAdded() if you wish to
|
||||||
|
* receive content updates as they arrive from the server or connect to KLLMReply::finished() if you prefer to have the whole
|
||||||
|
* message delivered at once.
|
||||||
|
*
|
||||||
|
* You should not request any completions (or otherwise use this class) until ready() returns true. Using the interface
|
||||||
|
* before it is ready can cause problems; for example, the interface may not have loaded the available models yet or the
|
||||||
|
* backend could be unreachable.
|
||||||
|
*/
|
||||||
|
class KLLMCORE_EXPORT KLLMInterface : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
Q_PROPERTY(bool ready READ ready NOTIFY readyChanged FINAL)
|
||||||
|
Q_PROPERTY(bool hasError READ hasError NOTIFY hasErrorChanged FINAL)
|
||||||
|
Q_PROPERTY(QStringList models READ models NOTIFY modelsChanged FINAL)
|
||||||
|
Q_PROPERTY(QString ollamaUrl READ ollamaUrl WRITE setOllamaUrl NOTIFY ollamaUrlChanged FINAL)
|
||||||
|
Q_PROPERTY(QString systemPrompt READ systemPrompt WRITE setSystemPrompt NOTIFY systemPromptChanged FINAL)
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Creates a KLLMInterface.
|
||||||
|
* @param parent The parent QObject.
|
||||||
|
*/
|
||||||
|
explicit KLLMInterface(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Creates a KLLMInterface with the url set to \a ollamaUrl.
|
||||||
|
* @param ollamaUrl The URL to the Ollama instance.
|
||||||
|
* @param parent The parent QObject.
|
||||||
|
*/
|
||||||
|
explicit KLLMInterface(const QString &ollamaUrl, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Creates a KLLMInterface with the url set to \a ollamaUrl.
|
||||||
|
* @param ollamaUrl The URL to the Ollama instance.
|
||||||
|
* @param parent The parent QObject.
|
||||||
|
*/
|
||||||
|
explicit KLLMInterface(const QUrl &ollamaUrl, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check whether the interface is ready.
|
||||||
|
*
|
||||||
|
* You should not use the interface until ready() returns true. Failure to observe this rule may result in undefined behavior.
|
||||||
|
*
|
||||||
|
* If the interface encounters an error, ready() will return false. However, do not use ready() to indicate to the user that the interface is in an error
|
||||||
|
* state, as the interface could be in the process of making its initial connection. Instead, you should use hasError() to check for an error state.
|
||||||
|
* Additionally, you should connect to errorOccurred() to handle errors as they arise.
|
||||||
|
*
|
||||||
|
* @return Returns whether the interface is ready.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] bool ready() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check whether the interface is in an error state.
|
||||||
|
*
|
||||||
|
* After you handle an error from errorEmitted(), you should monitor this property. When it becomes \c false, you can safely resume operations.
|
||||||
|
*
|
||||||
|
* @return Returns whether the interface is in an error state.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] bool hasError() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Retrieve a list of models supported by the LLM backend.
|
||||||
|
*
|
||||||
|
* When creating a KLLMRequest, you should choose a model from this list for the request. If you do not specify a model,
|
||||||
|
* the request will probably fail.
|
||||||
|
*
|
||||||
|
* @return Returns a QStringList containing all valid models for this interface.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QStringList models() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the URL to the Ollama instance.
|
||||||
|
* @return The URL for the Ollama instance.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QString ollamaUrl() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the URL to the Ollama instance.
|
||||||
|
*
|
||||||
|
* Since Ollama is a self-hostable service, users may wish to use different instances. Use this function to set the URL to the desired instance. It should
|
||||||
|
* \a not contain the \c /api portion of the URL.
|
||||||
|
*
|
||||||
|
* @param ollamaUrl The new URL for the Ollama instance.
|
||||||
|
*/
|
||||||
|
void setOllamaUrl(const QString &ollamaUrl);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A convenience overload of setOllamaUrl() that takes a QUrl.
|
||||||
|
* @param ollamaUrl The new URL for the Ollama instance.
|
||||||
|
*/
|
||||||
|
void setOllamaUrl(const QUrl &ollamaUrl);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the system prompt for the LLM.
|
||||||
|
* @return The system prompt string.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QString systemPrompt() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the system prompt for the LLM.
|
||||||
|
*
|
||||||
|
* LLMs can take system prompts that instruct them on how they should generally behave in a conversation. This could be anything from how they speak to what
|
||||||
|
* types of information they prefer to present. You can set a system prompt here to better cater to your users.
|
||||||
|
*
|
||||||
|
* @param systemPrompt The system prompt for the LLM.
|
||||||
|
*/
|
||||||
|
void setSystemPrompt(const QString &systemPrompt);
|
||||||
|
|
||||||
|
public Q_SLOTS:
|
||||||
|
/**
|
||||||
|
* @brief Request a completion from the LLM.
|
||||||
|
*
|
||||||
|
* Calling this function starts a request to the LLM backend. You should use the returned KLLMReply pointer to track the
|
||||||
|
* status of the LLM's response. Once the KLLMReply emits KLLMReply::finished(), it is your responsibility to either
|
||||||
|
* track or delete the KLLMReply; auto-deleting is not implemented yet.
|
||||||
|
*
|
||||||
|
* @param request The request object that will be used to create the actual LLM request.
|
||||||
|
* @return Returns a pointer to a KLLMReply that can be used to track the progress of the reply.
|
||||||
|
*/
|
||||||
|
KLLMReply *getCompletion(const KLLMRequest &request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Request model info from Ollama.
|
||||||
|
*
|
||||||
|
* Calling this function starts a request to the LLM backend. You should use the returned KLLMReply pointer to track the
|
||||||
|
* status of the LLM's response. Once the KLLMReply emits KLLMReply::finished(), it is your responsibility to either
|
||||||
|
* track or delete the KLLMReply; auto-deleting is not implemented yet.
|
||||||
|
*
|
||||||
|
* @param request The request object that will be used to create the actual LLM request.
|
||||||
|
* @return Returns a pointer to a KLLMReply that can be used to track the progress of the reply.
|
||||||
|
*/
|
||||||
|
KLLMReply *getModelInfo(const KLLMRequest &request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reload the LLM interface.
|
||||||
|
*
|
||||||
|
* Reloading the interface can be used to check if a network error is gone or to see if the available models have changed.
|
||||||
|
*/
|
||||||
|
void reload();
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
/**
|
||||||
|
* @brief This signal is emitted when any completion requested by the interface is completed.
|
||||||
|
* @param replyText Contains the text of the completion.
|
||||||
|
*/
|
||||||
|
void finished(const QString &replyText);
|
||||||
|
|
||||||
|
void readyChanged();
|
||||||
|
void hasErrorChanged();
|
||||||
|
void modelsChanged();
|
||||||
|
void ollamaUrlChanged();
|
||||||
|
void systemPromptChanged();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief An error occurred while communicating with the interface.
|
||||||
|
* @param message Contains the human readable error message.
|
||||||
|
*/
|
||||||
|
void errorOccurred(const QString &message);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QNetworkAccessManager *const m_manager;
|
||||||
|
QStringList m_models;
|
||||||
|
bool m_ready = false;
|
||||||
|
bool m_hasError = false;
|
||||||
|
QString m_ollamaUrl;
|
||||||
|
QString m_systemPrompt;
|
||||||
|
QMetaObject::Connection m_ollamaCheck;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Loren Burkholder <computersemiexpert@outlook.com>
|
||||||
|
// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#include "KLLMReply.h"
|
||||||
|
#include "kllmcore_debug.h"
|
||||||
|
|
||||||
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
using namespace Qt::StringLiterals;
|
||||||
|
using namespace KLLMCore;
|
||||||
|
|
||||||
|
KLLMReply::KLLMReply(QNetworkReply *netReply, QObject *parent, RequestTypes requestType)
|
||||||
|
: QObject{parent}
|
||||||
|
, m_reply{netReply}
|
||||||
|
, m_requestType{requestType}
|
||||||
|
{
|
||||||
|
connect(m_reply, &QNetworkReply::finished, m_reply, [this] {
|
||||||
|
// Normally, we could assume that the tokens will never be empty once the request finishes, but it could be possible
|
||||||
|
// that the request failed and we have no tokens to parse.
|
||||||
|
if (m_requestType == RequestTypes::StreamingGenerate && !m_tokens.empty()) {
|
||||||
|
const auto finalResponse = m_tokens.constLast();
|
||||||
|
m_context.setOllamaContext(finalResponse["context"_L1].toArray());
|
||||||
|
m_info.totalDuration = std::chrono::nanoseconds{finalResponse["total_duration"_L1].toVariant().toULongLong()};
|
||||||
|
m_info.loadDuration = std::chrono::nanoseconds{finalResponse["load_duration"_L1].toVariant().toULongLong()};
|
||||||
|
m_info.promptEvalTokenCount = finalResponse["prompt_eval_count"_L1].toVariant().toULongLong();
|
||||||
|
m_info.promptEvalDuration = std::chrono::nanoseconds{finalResponse["prompt_eval_duration"_L1].toVariant().toULongLong()};
|
||||||
|
m_info.tokenCount = finalResponse["eval_count"_L1].toVariant().toULongLong();
|
||||||
|
m_info.duration = std::chrono::nanoseconds{finalResponse["eval_duration"_L1].toVariant().toULongLong()};
|
||||||
|
}
|
||||||
|
|
||||||
|
qCDebug(KLLMCORE_LOG) << "Ollama response finished";
|
||||||
|
m_finished = true;
|
||||||
|
Q_EMIT finished();
|
||||||
|
});
|
||||||
|
connect(m_reply, &QNetworkReply::errorOccurred, m_reply, [](QNetworkReply::NetworkError e) {
|
||||||
|
qCDebug(KLLMCORE_LOG) << "Ollama HTTP error:" << e;
|
||||||
|
});
|
||||||
|
connect(m_reply, &QNetworkReply::downloadProgress, m_reply, [this](qint64 received, qint64 /*total*/) {
|
||||||
|
m_incompleteTokens += m_reply->read(received - m_receivedSize);
|
||||||
|
m_receivedSize = received;
|
||||||
|
switch (m_requestType) {
|
||||||
|
case RequestTypes::Show:
|
||||||
|
m_tokens.append(QJsonDocument::fromJson(m_incompleteTokens));
|
||||||
|
break;
|
||||||
|
case RequestTypes::StreamingGenerate:
|
||||||
|
auto completeTokens = m_incompleteTokens.split('\n');
|
||||||
|
if (completeTokens.size() <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_incompleteTokens = completeTokens.last();
|
||||||
|
completeTokens.removeLast();
|
||||||
|
|
||||||
|
m_tokens.reserve(completeTokens.count());
|
||||||
|
for (const auto &tok : std::as_const(completeTokens)) {
|
||||||
|
m_tokens.append(QJsonDocument::fromJson(tok));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Q_EMIT contentAdded();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QString KLLMReply::readResponse() const
|
||||||
|
{
|
||||||
|
QString ret;
|
||||||
|
switch (m_requestType) {
|
||||||
|
case RequestTypes::Show:
|
||||||
|
ret += QString::fromLatin1("## Template: \n```\n") + m_tokens.constFirst()["template"_L1].toString() + QString::fromLatin1("\n```\n");
|
||||||
|
ret += QString::fromLatin1("## Modelfile: \n```\n") + m_tokens.constFirst()["modelfile"_L1].toString() + QString::fromLatin1("\n```\n");
|
||||||
|
ret += QString::fromLatin1("## Parameters: \n```\n") + m_tokens.constFirst()["parameters"_L1].toString() + QString::fromLatin1("\n```\n");
|
||||||
|
ret += QString::fromLatin1("## Details: \n```\n")
|
||||||
|
+ QString::fromLatin1(QJsonDocument::fromVariant(m_tokens.constFirst()["details"_L1].toVariant()).toJson()) + QString::fromLatin1("\n```\n");
|
||||||
|
ret += QString::fromLatin1("## Model Info: \n```\n")
|
||||||
|
+ QString::fromLatin1(QJsonDocument::fromVariant(m_tokens.constFirst()["model_info"_L1].toVariant()).toJson()) + QString::fromLatin1("\n```\n");
|
||||||
|
break;
|
||||||
|
case RequestTypes::StreamingGenerate:
|
||||||
|
for (const auto &tok : m_tokens)
|
||||||
|
ret += tok["response"_L1].toString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KLLMContext &KLLMReply::context() const
|
||||||
|
{
|
||||||
|
return m_context;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KLLMReplyInfo &KLLMReply::info() const
|
||||||
|
{
|
||||||
|
return m_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KLLMReply::RequestTypes &KLLMReply::requestType() const
|
||||||
|
{
|
||||||
|
return m_requestType;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool KLLMReply::isFinished() const
|
||||||
|
{
|
||||||
|
return m_finished;
|
||||||
|
}
|
||||||
|
#include "moc_KLLMReply.cpp"
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Loren Burkholder <computersemiexpert@outlook.com>
|
||||||
|
// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "kllmcore_export.h"
|
||||||
|
#include <QJsonDocument>
|
||||||
|
|
||||||
|
#include "KLLMContext.h"
|
||||||
|
|
||||||
|
class QNetworkReply;
|
||||||
|
namespace KLLMCore
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief The KLLMReplyInfo class represents information about a reply from an LLM.
|
||||||
|
*
|
||||||
|
* When an LLM generates a completion, the server generally will return some information about the completion, including the
|
||||||
|
* duration of the completion, the number of tokens received, and the duration of the prompt evaluation. This struct encapsulates such information.
|
||||||
|
* If any one of these fields is not available, it will be set to its default value.
|
||||||
|
*/
|
||||||
|
struct KLLMCORE_EXPORT KLLMReplyInfo {
|
||||||
|
//! The total time from when the request was received by the server to when the reply was returned.
|
||||||
|
std::chrono::nanoseconds totalDuration;
|
||||||
|
|
||||||
|
//! The time spent loading the model.
|
||||||
|
std::chrono::nanoseconds loadDuration;
|
||||||
|
|
||||||
|
//! The number of tokens in the prompt.
|
||||||
|
int promptEvalTokenCount;
|
||||||
|
|
||||||
|
//! The time spent evaluating the prompt.
|
||||||
|
std::chrono::nanoseconds promptEvalDuration;
|
||||||
|
|
||||||
|
//! The number of tokens in the reply.
|
||||||
|
int tokenCount;
|
||||||
|
|
||||||
|
//! The time spent generating the reply.
|
||||||
|
std::chrono::nanoseconds duration;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The KLLMReply class represents a reply from an LLM.
|
||||||
|
*
|
||||||
|
* Requesting a completion from a KLLMInterface will return a KLLMReply. You can use this to track the progress of the LLM's
|
||||||
|
* reply.
|
||||||
|
*
|
||||||
|
* If you want to stream a reply as it is written in real time, connect to contentAdded() and use readResponse() to retrieve
|
||||||
|
* the new content. If you prefer to wait for the entire reply before displaying anything, connect to finished(), which will
|
||||||
|
* only be emitted once the reply is complete.
|
||||||
|
*/
|
||||||
|
class KLLMCORE_EXPORT KLLMReply : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Specifies the request type.
|
||||||
|
*
|
||||||
|
* When the class in instantiated the type of request should be specified
|
||||||
|
*/
|
||||||
|
enum class RequestTypes {
|
||||||
|
StreamingGenerate,
|
||||||
|
Show
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* @brief Get the current response content.
|
||||||
|
*
|
||||||
|
* This function returns what it has recieved of the response so far. Therefore, until finished() is emitted, this
|
||||||
|
* function may return different values. However, once finished() is emitted, the content is guaranteed to remain
|
||||||
|
* constant.
|
||||||
|
*
|
||||||
|
* @return The content that has been returned so far.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QString readResponse() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the context token for this response (if applicable).
|
||||||
|
*
|
||||||
|
* Messages sent by most LLMs have a context identifier that allows you to chain messages into a conversation. To create
|
||||||
|
* such a conversation, you need to take this context object and set it on the next KLLMRequest in the conversation.
|
||||||
|
* KLLMInterface::getCompletion() will use that context object to continue the message thread.
|
||||||
|
*
|
||||||
|
* @return A context object that refers to this response.
|
||||||
|
*/
|
||||||
|
const KLLMContext &context() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get extra information about the reply (if applicable).
|
||||||
|
*
|
||||||
|
* This function returns a KLLMReplyInfo object containing information about this reply. If the reply has not finished, the KLLMReplyInfo object will have
|
||||||
|
* all members set to their default values.
|
||||||
|
*
|
||||||
|
* @return Extra information about the reply.
|
||||||
|
*/
|
||||||
|
const KLLMReplyInfo &info() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check whether the reply has finished.
|
||||||
|
*
|
||||||
|
* If you need to know if the response has finished changing or if the context has been received yet, call this function.
|
||||||
|
*
|
||||||
|
* @return Whether the reply has finished.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] bool isFinished() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get request type.
|
||||||
|
*
|
||||||
|
* The request type is set when this object is created.
|
||||||
|
*
|
||||||
|
* @return Corresponding request type.
|
||||||
|
*/
|
||||||
|
const RequestTypes &requestType() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
explicit KLLMReply(QNetworkReply *netReply, QObject *parent = nullptr, RequestTypes requestType = RequestTypes::StreamingGenerate);
|
||||||
|
|
||||||
|
friend class KLLMInterface;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
/**
|
||||||
|
* @brief Emits when new content has been added to the response.
|
||||||
|
*
|
||||||
|
* If you are not streaming the response live, this signal is not of importance to you. However, if you are streaming
|
||||||
|
* content, when this signal is emitted, you should call readResponse() to update the response that your application
|
||||||
|
* shows.
|
||||||
|
*/
|
||||||
|
void contentAdded();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Emits when the LLM has finished returning its response.
|
||||||
|
*
|
||||||
|
* After this signal has emitted, the content is guaranteed to not change. At this point, you should call readResponse()
|
||||||
|
* to get the content and then either take ownership of the KLLMReply or delete it, as automatic reply deletion is not
|
||||||
|
* implemented yet.
|
||||||
|
*/
|
||||||
|
void finished();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QNetworkReply *const m_reply;
|
||||||
|
QByteArray m_incompleteTokens;
|
||||||
|
|
||||||
|
QList<QJsonDocument> m_tokens;
|
||||||
|
|
||||||
|
KLLMContext m_context;
|
||||||
|
KLLMReplyInfo m_info;
|
||||||
|
RequestTypes m_requestType = RequestTypes::StreamingGenerate;
|
||||||
|
|
||||||
|
int m_receivedSize = 0;
|
||||||
|
bool m_finished = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Loren Burkholder <computersemiexpert@outlook.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#include "KLLMRequest.h"
|
||||||
|
|
||||||
|
using namespace KLLMCore;
|
||||||
|
KLLMRequest::KLLMRequest(const QString &message)
|
||||||
|
: m_message{message}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QString KLLMRequest::message() const
|
||||||
|
{
|
||||||
|
return m_message;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KLLMContext &KLLMRequest::context() const
|
||||||
|
{
|
||||||
|
return m_context;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString KLLMRequest::model() const
|
||||||
|
{
|
||||||
|
return m_model;
|
||||||
|
}
|
||||||
|
|
||||||
|
void KLLMRequest::setMessage(const QString &message)
|
||||||
|
{
|
||||||
|
m_message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
void KLLMRequest::setContext(const KLLMContext &context)
|
||||||
|
{
|
||||||
|
m_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
void KLLMRequest::setModel(const QString &model)
|
||||||
|
{
|
||||||
|
m_model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDebug operator<<(QDebug d, const KLLMRequest &t)
|
||||||
|
{
|
||||||
|
d << "Model: " << t.model();
|
||||||
|
d << "Message: " << t.message();
|
||||||
|
d << "Context: " << t.context();
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Loren Burkholder <computersemiexpert@outlook.com>
|
||||||
|
// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "kllmcore_export.h"
|
||||||
|
|
||||||
|
#include "KLLMContext.h"
|
||||||
|
#include <QDebug>
|
||||||
|
namespace KLLMCore
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief The KLLMRequest class contains a representation of a message to an LLM.
|
||||||
|
*
|
||||||
|
* Before calling KLLMInterface::getCompletion(), you need to create a KLLMRequest with information about the desired
|
||||||
|
* completion.
|
||||||
|
*/
|
||||||
|
class KLLMCORE_EXPORT KLLMRequest
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Creates a KLLMRequest with the message \a message.
|
||||||
|
* @param message The message that will be sent to the LLM.
|
||||||
|
* @param parent
|
||||||
|
*/
|
||||||
|
explicit KLLMRequest(const QString &message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Use this to get the message for the LLM.
|
||||||
|
* @return Returns the message to be sent to the LLM.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QString message() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Use this to retrive the context for the LLM.
|
||||||
|
* @return Returns the context object to be sent to the LLM.
|
||||||
|
*/
|
||||||
|
const KLLMContext &context() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Gets the LLM model that will be used by the backend.
|
||||||
|
* @return The name of the model to be used for this request.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QString model() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sets the message to be sent to the LLM.
|
||||||
|
* @param message The message text to be sent to the LLM.
|
||||||
|
*/
|
||||||
|
void setMessage(const QString &message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sets the conversation context for the LLM.
|
||||||
|
*
|
||||||
|
* If you want the LLM to respond in context of previous messages, you should set this to the context returned in the
|
||||||
|
* previous KLLMReply.
|
||||||
|
*
|
||||||
|
* @param context The context object for this request.
|
||||||
|
*/
|
||||||
|
void setContext(const KLLMContext &context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sets the model to be used for this request.
|
||||||
|
*
|
||||||
|
* You should set this to one of the values returned by KLLMInterface::models(); failure to do so will likely produce an
|
||||||
|
* error from the backend.
|
||||||
|
*
|
||||||
|
* @param model The name of the model to be used for this request.
|
||||||
|
*/
|
||||||
|
void setModel(const QString &model);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_message;
|
||||||
|
KLLMContext m_context;
|
||||||
|
QString m_model;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
KLLMCORE_EXPORT QDebug operator<<(QDebug d, const KLLMCore::KLLMRequest &t);
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
|
||||||
|
set(KOMPANION_AI_SRCS
|
||||||
|
Client/KIClient.cpp
|
||||||
|
Provider/OllamaProvider.cpp
|
||||||
|
Completion/KIReply.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set(KOMPANION_AI_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_ai STATIC ${KOMPANION_AI_SRCS} ${KOMPANION_AI_HDRS})
|
||||||
|
|
||||||
|
target_include_directories(kom_ai PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
|
target_link_libraries(kom_ai PUBLIC
|
||||||
|
Qt6::Core
|
||||||
|
Qt6::Network
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
|
||||||
|
#include "KIClient.h"
|
||||||
|
|
||||||
|
namespace KompanionAI {
|
||||||
|
|
||||||
|
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 KompanionAI
|
||||||
|
|
@ -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 KompanionAI {
|
||||||
|
|
||||||
|
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 KompanionAI
|
||||||
|
|
||||||
|
#endif // KIANICLIENT_H
|
||||||
|
|
@ -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 KompanionAI {
|
||||||
|
|
||||||
|
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 KompanionAI
|
||||||
|
|
||||||
|
#endif // KIANICHATOPTIONS_H
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
#ifndef KIANIERROR_H
|
||||||
|
#define KIANIERROR_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace KompanionAI {
|
||||||
|
|
||||||
|
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 KompanionAI
|
||||||
|
|
||||||
|
#endif // KIANIERROR_H
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
#include "KIReply.h"
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
namespace KompanionAI {
|
||||||
|
|
||||||
|
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 KompanionAI
|
||||||
|
|
@ -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 KompanionAI {
|
||||||
|
|
||||||
|
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 KompanionAI
|
||||||
|
|
||||||
|
#endif // KIANIREPLY_H
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
|
||||||
|
#ifndef KIANIEMBEDDING_H
|
||||||
|
#define KIANIEMBEDDING_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
namespace KompanionAI {
|
||||||
|
|
||||||
|
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 KompanionAI
|
||||||
|
|
||||||
|
#endif // KIANIEMBEDDING_H
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
|
||||||
|
#ifndef KIANIMESSAGE_H
|
||||||
|
#define KIANIMESSAGE_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QList>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
namespace KompanionAI {
|
||||||
|
|
||||||
|
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 KompanionAI
|
||||||
|
|
||||||
|
#endif // KIANIMESSAGE_H
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
|
||||||
|
#ifndef KIANITHREAD_H
|
||||||
|
#define KIANITHREAD_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QList>
|
||||||
|
#include "KIMessage.h"
|
||||||
|
|
||||||
|
namespace KompanionAI {
|
||||||
|
|
||||||
|
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 KompanionAI
|
||||||
|
|
||||||
|
#endif // KIANITHREAD_H
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
#ifndef KIANIPOLICY_H
|
||||||
|
#define KIANIPOLICY_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace KompanionAI {
|
||||||
|
|
||||||
|
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 KompanionAI
|
||||||
|
|
||||||
|
#endif // KIANIPOLICY_H
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
#ifndef KIANICAPABILITIES_H
|
||||||
|
#define KIANICAPABILITIES_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
namespace KompanionAI {
|
||||||
|
|
||||||
|
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 KompanionAI
|
||||||
|
|
||||||
|
#endif // KIANICAPABILITIES_H
|
||||||
|
|
@ -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 KompanionAI {
|
||||||
|
|
||||||
|
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 KompanionAI
|
||||||
|
|
||||||
|
#endif // KIANIPROVIDER_H
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
#include "OllamaProvider.h"
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include "../Completion/KIChatOptions.h" // Added
|
||||||
|
#include "../Completion/KIError.h" // Added
|
||||||
|
|
||||||
|
namespace KompanionAI {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OllamaProvider::reload()
|
||||||
|
{
|
||||||
|
QNetworkRequest req{QUrl(QStringLiteral("http://localhost:11434/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(QStringLiteral("http://localhost:11434/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)
|
||||||
|
{
|
||||||
|
QNetworkRequest req{QUrl(QStringLiteral("http://localhost:11434/api/embeddings"))};
|
||||||
|
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||||
|
|
||||||
|
QJsonObject data;
|
||||||
|
data["model"] = opts.model;
|
||||||
|
data["prompt"] = texts.join("\n"); // Join all texts into a single prompt
|
||||||
|
|
||||||
|
auto netReply = m_manager->post(req, QJsonDocument(data).toJson());
|
||||||
|
|
||||||
|
QFutureInterface<KIEmbeddingResult> interface;
|
||||||
|
interface.reportStarted();
|
||||||
|
|
||||||
|
connect(netReply, &QNetworkReply::finished, this, [netReply, interface]() mutable {
|
||||||
|
if (netReply->error() != QNetworkReply::NoError) {
|
||||||
|
// TODO: Handle error
|
||||||
|
interface.reportFinished();
|
||||||
|
netReply->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto json = QJsonDocument::fromJson(netReply->readAll());
|
||||||
|
const auto embeddingArray = json["embedding"].toArray();
|
||||||
|
|
||||||
|
KIEmbeddingResult result;
|
||||||
|
QVector<float> embedding;
|
||||||
|
for (const QJsonValue &value : embeddingArray) {
|
||||||
|
embedding.push_back(value.toDouble());
|
||||||
|
}
|
||||||
|
result.vectors.push_back(embedding);
|
||||||
|
result.model = json["model"].toString();
|
||||||
|
|
||||||
|
interface.reportResult(result);
|
||||||
|
interface.reportFinished();
|
||||||
|
netReply->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
return interface.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OllamaProvider::cancel(quint64 requestId)
|
||||||
|
{
|
||||||
|
Q_UNUSED(requestId);
|
||||||
|
// TODO: Implement cancellation logic
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace KompanionAI
|
||||||
|
|
@ -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 KompanionAI {
|
||||||
|
|
||||||
|
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 KompanionAI
|
||||||
|
|
||||||
|
#endif // OLLAMAPROVIDER_H
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
|
||||||
|
#ifndef KIANITOOL_H
|
||||||
|
#define KIANITOOL_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QList>
|
||||||
|
#include <QVariant>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
namespace KompanionAI {
|
||||||
|
|
||||||
|
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 KompanionAI
|
||||||
|
|
||||||
|
#endif // KIANITOOL_H
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,39 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Loren Burkholder <computersemiexpert@outlook.com>
|
||||||
|
// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QList>
|
||||||
|
|
||||||
|
#include "KIToolSpec.h"
|
||||||
|
#include "KIPolicy.h"
|
||||||
|
|
||||||
|
namespace KompanionAI
|
||||||
|
{
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Loren Burkholder <computersemiexpert@outlook.com>
|
||||||
|
// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace KompanionAI
|
||||||
|
{
|
||||||
|
|
||||||
|
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";
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Loren Burkholder <computersemiexpert@outlook.com>
|
||||||
|
// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QVector>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace KompanionAI
|
||||||
|
{
|
||||||
|
|
||||||
|
class KIEmbeddingResult
|
||||||
|
{
|
||||||
|
Q_GADGET
|
||||||
|
|
||||||
|
public:
|
||||||
|
QVector<QVector<float>> vectors;
|
||||||
|
QString model;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Loren Burkholder <computersemiexpert@outlook.com>
|
||||||
|
// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace KompanionAI
|
||||||
|
{
|
||||||
|
|
||||||
|
class KIError
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum ErrorCode {
|
||||||
|
NoError,
|
||||||
|
NetworkError,
|
||||||
|
ApiError,
|
||||||
|
InvalidJson,
|
||||||
|
Cancelled
|
||||||
|
};
|
||||||
|
|
||||||
|
KIError(ErrorCode code = NoError, const QString &message = QString(), int httpStatus = 0, int retryAfter = 0)
|
||||||
|
: m_code(code)
|
||||||
|
, m_message(message)
|
||||||
|
, m_httpStatus(httpStatus)
|
||||||
|
, m_retryAfter(retryAfter)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorCode code() const { return m_code; }
|
||||||
|
QString message() const { return m_message; }
|
||||||
|
int httpStatus() const { return m_httpStatus; }
|
||||||
|
int retryAfter() const { return m_retryAfter; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
ErrorCode m_code;
|
||||||
|
QString m_message;
|
||||||
|
int m_httpStatus;
|
||||||
|
int m_retryAfter;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Loren Burkholder <computersemiexpert@outlook.com>
|
||||||
|
// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QList>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
namespace KompanionAI
|
||||||
|
{
|
||||||
|
|
||||||
|
class KIMessagePart
|
||||||
|
{
|
||||||
|
Q_GADGET
|
||||||
|
Q_PROPERTY(QString mime READ mime)
|
||||||
|
Q_PROPERTY(QString text READ text)
|
||||||
|
|
||||||
|
public:
|
||||||
|
QString mime;
|
||||||
|
QString text;
|
||||||
|
};
|
||||||
|
|
||||||
|
class KIMessage
|
||||||
|
{
|
||||||
|
Q_GADGET
|
||||||
|
Q_PROPERTY(QString role READ role)
|
||||||
|
Q_PROPERTY(QList<KIMessagePart> parts READ parts)
|
||||||
|
|
||||||
|
public:
|
||||||
|
QString role;
|
||||||
|
QList<KIMessagePart> parts;
|
||||||
|
QVariantMap metadata;
|
||||||
|
};
|
||||||
|
|
||||||
|
class KIThread
|
||||||
|
{
|
||||||
|
Q_GADGET
|
||||||
|
Q_PROPERTY(QList<KIMessage> messages READ messages)
|
||||||
|
|
||||||
|
public:
|
||||||
|
QList<KIMessage> messages;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Loren Burkholder <computersemiexpert@outlook.com>
|
||||||
|
// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace KompanionAI
|
||||||
|
{
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Loren Burkholder <computersemiexpert@outlook.com>
|
||||||
|
// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QFuture>
|
||||||
|
|
||||||
|
#include "KIReply.h"
|
||||||
|
#include "KIThread.h"
|
||||||
|
#include "KIChatOptions.h"
|
||||||
|
#include "KIEmbeddingResult.h"
|
||||||
|
#include "KIEmbedOptions.h"
|
||||||
|
#include "KICapabilities.h"
|
||||||
|
|
||||||
|
namespace KompanionAI
|
||||||
|
{
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void modelsChanged();
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Loren Burkholder <computersemiexpert@outlook.com>
|
||||||
|
// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
#include "KIToolSpec.h"
|
||||||
|
|
||||||
|
namespace KompanionAI
|
||||||
|
{
|
||||||
|
|
||||||
|
class KIError;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
Q_INVOKABLE QString text() const;
|
||||||
|
bool isFinished() const;
|
||||||
|
int promptTokens() const;
|
||||||
|
int completionTokens() const;
|
||||||
|
QString model() const;
|
||||||
|
|
||||||
|
Q_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();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool m_finished = false;
|
||||||
|
int m_promptTokens = 0;
|
||||||
|
int m_completionTokens = 0;
|
||||||
|
QString m_model;
|
||||||
|
QString m_text;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Loren Burkholder <computersemiexpert@outlook.com>
|
||||||
|
// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QList>
|
||||||
|
#include <QVariant>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
namespace KompanionAI
|
||||||
|
{
|
||||||
|
|
||||||
|
class KIToolParam
|
||||||
|
{
|
||||||
|
Q_GADGET
|
||||||
|
Q_PROPERTY(QString name READ name)
|
||||||
|
Q_PROPERTY(QString type READ type)
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
public:
|
||||||
|
QString name;
|
||||||
|
QVariant result;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -3,11 +3,11 @@ MCP backend for Kompanion: memory/context/embedding provider over MCP, built fro
|
||||||
|
|
||||||
> ## 📈 Project Summary
|
> ## 📈 Project Summary
|
||||||
>
|
>
|
||||||
> **✅ Done**: 1 | **🔄 In Progress**: 0 | **⬜ Todo**: 28 | **❌ Blocked**: 0
|
> **✅ Done**: 2 | **🔄 In Progress**: 0 | **⬜ Todo**: 38 | **❌ Blocked**: 0
|
||||||
>
|
>
|
||||||
> **Progress**: 3% `█░░░░░░░░░░░░░░░░░░░` 1/29 tasks
|
> **Progress**: 5% `█░░░░░░░░░░░░░░░░░░░` 2/40 tasks
|
||||||
>
|
>
|
||||||
> **Priorities**: 🚨 **Critical**: 0 | 🔴 **High**: 1 | 🟡 **Medium**: 29 | 🟢 **Low**: 0
|
> **Priorities**: 🚨 **Critical**: 0 | 🔴 **High**: 1 | 🟡 **Medium**: 41 | 🟢 **Low**: 0
|
||||||
|
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
|
|
@ -16,7 +16,7 @@ MCP backend for Kompanion: memory/context/embedding provider over MCP, built fro
|
||||||
| #1 | ⬜ todo | 700 | **Project Setup: metal-kompanion-mcp** | MCP backend for Kompanion: me... |
|
| #1 | ⬜ todo | 700 | **Project Setup: metal-kompanion-mcp** | MCP backend for Kompanion: me... |
|
||||||
| #2 | ⬜ in_progress | 500 | **Design MCP memory/context API** | Specify MCP tools for: save_c... |
|
| #2 | ⬜ in_progress | 500 | **Design MCP memory/context API** | Specify MCP tools for: save_c... |
|
||||||
| #3 | ⬜ todo | 501 | **Select embedding backend & storage** | Choose between local (Ollama/... |
|
| #3 | ⬜ todo | 501 | **Select embedding backend & storage** | Choose between local (Ollama/... |
|
||||||
| #4 | ✅ done | 499 | **Scaffold qtmcp-based server** | Set up C++/Qt MCP server skeleton with qtmcp backend. |
|
| #4 | ⬜ in_progress | 499 | **Scaffold qtmcp-based server** | Set up C++/Qt MCP server skel... |
|
||||||
| #5 | ⬜ todo | 502 | **Implement memory adapters** | Adapters: (1) SQLite+FAISS/pg... |
|
| #5 | ⬜ todo | 502 | **Implement memory adapters** | Adapters: (1) SQLite+FAISS/pg... |
|
||||||
| #6 | ⬜ todo | 498 | **Deep research: memory DB architecture & schema** | Survey best practices for con... |
|
| #6 | ⬜ todo | 498 | **Deep research: memory DB architecture & schema** | Survey best practices for con... |
|
||||||
| #7 | ⬜ todo | 503 | **Decide primary DB: Postgres+pgvector vs SQLite+FAISS** | Evaluate tradeoffs (multi-use... |
|
| #7 | ⬜ todo | 503 | **Decide primary DB: Postgres+pgvector vs SQLite+FAISS** | Evaluate tradeoffs (multi-use... |
|
||||||
|
|
@ -29,10 +29,14 @@ MCP backend for Kompanion: memory/context/embedding provider over MCP, built fro
|
||||||
| #14 | ⬜ todo | 494 | **Cloud adapters: backup/sync & payments stubs** | Expose kom.cloud.v1.backup.up... |
|
| #14 | ⬜ todo | 494 | **Cloud adapters: backup/sync & payments stubs** | Expose kom.cloud.v1.backup.up... |
|
||||||
| #15 | ⬜ todo | 507 | **Purge job & admin delete paths** | Implement scheduled hard-dele... |
|
| #15 | ⬜ todo | 507 | **Purge job & admin delete paths** | Implement scheduled hard-dele... |
|
||||||
| #16 | ⬜ todo | 493 | **Test suite: privacy & hybrid search** | Cross-tenant leakage, redacti... |
|
| #16 | ⬜ todo | 493 | **Test suite: privacy & hybrid search** | Cross-tenant leakage, redacti... |
|
||||||
|
| #17 | ⬜ todo | 508 | **Enable Qwen-2.5-Coder with tool support (Happy-Code profile)** | Prepare system prompt + regis... |
|
||||||
|
| #18 | ⬜ todo | 492 | **Expose Agentic-Control-Framework as a tool** | Wrap ACF endpoints into a too... |
|
||||||
| #19 | ⬜ todo | 509 | **DAL skeleton + SQL calls (pgvector)** | Create DAL interfaces and pgv... |
|
| #19 | ⬜ todo | 509 | **DAL skeleton + SQL calls (pgvector)** | Create DAL interfaces and pgv... |
|
||||||
| #21 | ⬜ todo | 510 | **DAL Phase 1: Qt6/QSql wiring + SQL calls** | Use QPSQL via Qt6::Sql, implement PgDal ag... |
|
| #20 | ⬜ todo | 491 | **Claude Code integration rescue plan** | Stabilize Qwen-2.5-Coder insi... |
|
||||||
|
| #21 | ⬜ todo | 510 | **DAL Phase 1: libpq/pqxx wiring + SQL calls** | Link pqxx, implement PgDal ag... |
|
||||||
| #22 | ⬜ todo | 490 | **Handlers → DAL integration** | Wire kom.memory.v1.upsert_mem... |
|
| #22 | ⬜ todo | 490 | **Handlers → DAL integration** | Wire kom.memory.v1.upsert_mem... |
|
||||||
| #23 | ⬜ todo | 511 | **Contract tests: DAL-backed tools** | Expand CTest to cover DAL-bac... |
|
| #23 | ⬜ todo | 511 | **Contract tests: DAL-backed tools** | Expand CTest to cover DAL-bac... |
|
||||||
|
| #24 | ⬜ todo | 489 | **Implement KompanionAI SDK** | |
|
||||||
|
|
||||||
|
|
||||||
### Task #2: Design MCP memory/context API - Subtasks
|
### Task #2: Design MCP memory/context API - Subtasks
|
||||||
|
|
@ -41,6 +45,14 @@ MCP backend for Kompanion: memory/context/embedding provider over MCP, built fro
|
||||||
|:--:|:------:|:------|
|
|:--:|:------:|:------|
|
||||||
| #2.1 | ⬜ todo | Write JSON Schemas for tools (done) |
|
| #2.1 | ⬜ todo | Write JSON Schemas for tools (done) |
|
||||||
|
|
||||||
|
### Task #21: DAL Phase 1: libpq/pqxx wiring + SQL calls - Subtasks
|
||||||
|
|
||||||
|
| ID | Status | Title |
|
||||||
|
|:--:|:------:|:------|
|
||||||
|
| #21.1 | ⬜ todo | CMake: find_package(pqxx) and link; CI env var DSN |
|
||||||
|
| #21.2 | ⬜ todo | PgDal: implement connect/tx + prepared statements |
|
||||||
|
| #21.3 | ⬜ todo | SQL: ensureNamespace, upsertItem/Chunks/Embeddings |
|
||||||
|
| #21.4 | ⬜ todo | Search: FTS/trgm + vector <-> with filters (namespace/thread/tags) |
|
||||||
|
|
||||||
### Task #22: Handlers → DAL integration - Subtasks
|
### Task #22: Handlers → DAL integration - Subtasks
|
||||||
|
|
||||||
|
|
@ -49,3 +61,19 @@ MCP backend for Kompanion: memory/context/embedding provider over MCP, built fro
|
||||||
| #22.1 | ⬜ todo | Replace ad-hoc JSON with parser (nlohmann/json or simdjson) |
|
| #22.1 | ⬜ todo | Replace ad-hoc JSON with parser (nlohmann/json or simdjson) |
|
||||||
| #22.2 | ⬜ todo | Validate request bodies against schemas before DAL calls |
|
| #22.2 | ⬜ todo | Validate request bodies against schemas before DAL calls |
|
||||||
| #22.3 | ⬜ todo | Scope & sensitivity enforcement (namespace/user + skip secret embeddings) |
|
| #22.3 | ⬜ todo | Scope & sensitivity enforcement (namespace/user + skip secret embeddings) |
|
||||||
|
|
||||||
|
### Task #24: Implement KompanionAI SDK - Subtasks
|
||||||
|
|
||||||
|
| ID | Status | Title |
|
||||||
|
|:--:|:------:|:------|
|
||||||
|
| #24.1 | ✅ done | Define Message & Thread Model |
|
||||||
|
| #24.2 | ✅ done | Implement Tool / Function Calling |
|
||||||
|
| #24.3 | ⬜ todo | Implement Provider abstraction (multi-backend) |
|
||||||
|
| #24.4 | ⬜ todo | Implement Completion / Reply / Streaming Events |
|
||||||
|
| #24.5 | ⬜ todo | Implement Options / Policies / Privacy |
|
||||||
|
| #24.6 | ⬜ todo | Implement Embeddings (for RAG / memory) |
|
||||||
|
| #24.7 | ⬜ todo | Implement Agent Loop Conveniences |
|
||||||
|
| #24.8 | ⬜ todo | Implement Error Model & Cancellation |
|
||||||
|
| #24.9 | ⬜ todo | Expose to QML |
|
||||||
|
| #24.10 | ⬜ todo | Migrate KLLM to KompanionAI |
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue