Compare commits
No commits in common. "c1d8c582f9b683e14d43ff543398fe39d3fb825a" and "3f1410a0951b984c239945de046b48188f373baf" have entirely different histories.
c1d8c582f9
...
3f1410a095
|
|
@ -1,9 +1,6 @@
|
||||||
# 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.
|
||||||
|
|
||||||
|
|
||||||
source dev.env for envrionment variables.
|
|
||||||
|
|
||||||
## MCP Usage
|
## MCP Usage
|
||||||
- This project uses agentic-control-framework Use this for task planning
|
- This project uses agentic-control-framework Use this for task planning
|
||||||
|
|
||||||
|
|
|
||||||
71
dev.env
71
dev.env
|
|
@ -1,71 +0,0 @@
|
||||||
|
|
||||||
# Our prefix
|
|
||||||
export CUSTOM_PREFIX=$HOME/dev/metal
|
|
||||||
|
|
||||||
# Start with fresh variables
|
|
||||||
unset LD_LIBRARY_PATH
|
|
||||||
unset PKG_CONFIG_PATH
|
|
||||||
|
|
||||||
function trim_space {
|
|
||||||
sed -i 's/[[:space:]]*$//' "$@"
|
|
||||||
}
|
|
||||||
prepend() { [ -d "$2" ] && eval $1=\"$2\$\{$1:+':'\$$1\}\" && export $1 ; }
|
|
||||||
|
|
||||||
|
|
||||||
# CPU core count (portable)
|
|
||||||
_nproc() {
|
|
||||||
if command -v nproc >/dev/null 2>&1; then nproc
|
|
||||||
elif getconf _NPROCESSORS_ONLN >/dev/null 2>&1; then getconf _NPROCESSORS_ONLN
|
|
||||||
else echo 1; fi
|
|
||||||
}
|
|
||||||
|
|
||||||
export PATH=$HOME/bin:$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin
|
|
||||||
|
|
||||||
function addprefix()
|
|
||||||
{
|
|
||||||
prepend PATH "$1/bin"
|
|
||||||
prepend LD_LIBRARY_PATH "$1/lib"
|
|
||||||
prepend LD_LIBRARY_PATH "$1/lib64"
|
|
||||||
prepend LD_LIBRARY_PATH "$1/lib/x86_64-linux-gnu"
|
|
||||||
prepend PKG_CONFIG_PATH "$1/lib64/pkgconfig"
|
|
||||||
prepend PKG_CONFIG_PATH "$1/lib/pkgconfig"
|
|
||||||
prepend PKG_CONFIG_PATH "$1/lib/x86_64-linux-gnu/pkgconfig"
|
|
||||||
prepend CMAKE_PREFIX_PATH "$1"
|
|
||||||
prepend CMAKE_PREFIX_PATH "$1/lib/cmake"
|
|
||||||
prepend CMAKE_PREFIX_PATH "$1/lib/x86_64-linux-gnu/cmake"
|
|
||||||
prepend CMAKE_MODULE_PATH "$1/lib/x86_64-linux-gnu/cmake"
|
|
||||||
prepend PYTHONPATH "$1/lib/python3.13"
|
|
||||||
}
|
|
||||||
|
|
||||||
addprefix $CUSTOM_PREFIX
|
|
||||||
export PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1
|
|
||||||
export PKG_CONFIG_ALLOW_SYSTEM_LIBS=1
|
|
||||||
|
|
||||||
# npm local (“global”) installs under CUSTOM_PREFIX
|
|
||||||
export NPM_CONFIG_PREFIX="$CUSTOM_PREFIX"
|
|
||||||
export NODE_PATH="$CUSTOM_PREFIX/lib/node_modules"
|
|
||||||
|
|
||||||
# Load a common venv
|
|
||||||
source $CUSTOM_PREFIX/pyenv/bin/activate
|
|
||||||
export PS1="(metal) $PS1"
|
|
||||||
|
|
||||||
# required for devfunctions
|
|
||||||
export BUILD_PREFIX=$CUSTOM_PREFIX/build
|
|
||||||
export SRC_PREFIX=$CUSTOM_PREFIX/src
|
|
||||||
|
|
||||||
source ~/scripts/devfunctions.sh
|
|
||||||
|
|
||||||
export MODELS=/mnt/data/models
|
|
||||||
export PG_DSN='postgresql://kompanion/kompanion?host=/var/run/postgresql'
|
|
||||||
export OLLAMA_MODELS="/mnt/bulk/models/ollama"
|
|
||||||
export OLLAMA_BASE_URL=127.0.0.1:11434
|
|
||||||
export LC_ALL=en_US.UTF-8
|
|
||||||
export QT_PLUGIN_PATH=$CUSTOM_PREFIX/lib/plugins:$CUSTOM_PREFIX/lib64/plugins:$CUSTOM_PREFIX/lib/x86_64-linux-gnu/qt6/plugins:$QTDIR/plugins:$QT_PLUGIN_PATH
|
|
||||||
export QML2_IMPORT_PATH=$CUSTOM_PREFIX/lib/qml:$CUSTOM_PREFIX/lib64/qml:$CUSTOM_PREFIX/lib/x86_64-linux-gnu/qml:$QTDIR/qml
|
|
||||||
export QML_IMPORT_PATH=$QML2_IMPORT_PATH
|
|
||||||
|
|
||||||
export LD_LIBRARY_PATH
|
|
||||||
export PKG_CONFIG_PATH
|
|
||||||
export CMAKE_PREFIX_PATH
|
|
||||||
export CMAKE_MODULE_PATH
|
|
||||||
export QT_MESSAGE_PATTERN=[32m%{time h:mm:ss.zzz}%{if-category}[32m %{category}:%{endif} %{if-debug}[35m%{function}%{endif}%{if-warning}[33m%{backtrace depth=5}%{endif}%{if-critical}[31m%{backtrace depth=3}%{endif}%{if-fatal}[31m%{backtrace depth=3}%{endif}[0m %{message}
|
|
||||||
Binary file not shown.
|
|
@ -594,53 +594,6 @@ inline std::string save_context(const std::string& reqJson) {
|
||||||
return os.str();
|
return os.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* upsert_and_embed
|
|
||||||
* Request: { namespace, model?, items: [{id?, text, tags?, metadata?}] }
|
|
||||||
* Response: { upserted, embedded }
|
|
||||||
*/
|
|
||||||
inline std::string upsert_and_embed(const std::string& reqJson) {
|
|
||||||
const std::string nsName = detail::extract_string_field(reqJson, "namespace");
|
|
||||||
if (nsName.empty()) return detail::error_response("bad_request","namespace is required");
|
|
||||||
auto nsRow = detail::database().ensureNamespace(nsName);
|
|
||||||
if (!nsRow) return detail::error_response("internal_error","failed to ensure namespace");
|
|
||||||
|
|
||||||
auto items = detail::parse_items(reqJson);
|
|
||||||
if (items.empty()) return detail::error_response("bad_request","items array must contain at least one entry");
|
|
||||||
std::string model = detail::extract_string_field(reqJson, "model");
|
|
||||||
|
|
||||||
// Upsert items first and collect texts/ids
|
|
||||||
std::vector<std::string> itemIds; itemIds.reserve(items.size());
|
|
||||||
std::vector<std::string> texts; texts.reserve(items.size());
|
|
||||||
for (auto &it : items) {
|
|
||||||
ki::ItemRow row; row.id = it.id; row.namespace_id = nsRow->id; row.text = it.text;
|
|
||||||
row.tags = it.tags; row.revision = 1; row.metadata_json = it.metadataJson.empty()?"{}":it.metadataJson; row.content_json = it.rawJson;
|
|
||||||
const std::string id = detail::database().upsertItem(row);
|
|
||||||
itemIds.push_back(id); texts.push_back(it.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Embed via libKI
|
|
||||||
KI::KIClient client; KI::OllamaProvider provider; client.setProvider(&provider);
|
|
||||||
KI::KIEmbedOptions opts; if (!model.empty()) opts.model = QString::fromStdString(model);
|
|
||||||
QStringList qtexts; for (auto &t : texts) qtexts.push_back(QString::fromStdString(t));
|
|
||||||
QEventLoop loop; QFuture<KI::KIEmbeddingResult> fut = client.embed(qtexts, opts);
|
|
||||||
QFutureWatcher<KI::KIEmbeddingResult> watcher; QObject::connect(&watcher, &QFutureWatcher<KI::KIEmbeddingResult>::finished, &loop, &QEventLoop::quit); watcher.setFuture(fut); loop.exec();
|
|
||||||
const KI::KIEmbeddingResult result = watcher.result();
|
|
||||||
|
|
||||||
// Upsert chunks + embeddings (ord=0)
|
|
||||||
int embedded = 0;
|
|
||||||
const int n = std::min(itemIds.size(), (std::size_t)result.vectors.size());
|
|
||||||
for (int i = 0; i < n; ++i) {
|
|
||||||
ki::ChunkRow chunk; chunk.item_id = itemIds[(size_t)i]; chunk.ord = 0; chunk.text = texts[(size_t)i];
|
|
||||||
auto chunkIds = detail::database().upsertChunks(std::vector<ki::ChunkRow>{chunk}); if (chunkIds.empty()) continue;
|
|
||||||
ki::EmbeddingRow emb; emb.chunk_id = chunkIds.front(); emb.model = result.model.toStdString(); emb.dim = result.vectors[i].size();
|
|
||||||
emb.vector.assign(result.vectors[i].begin(), result.vectors[i].end());
|
|
||||||
detail::database().upsertEmbeddings(std::vector<ki::EmbeddingRow>{emb}); embedded++;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::ostringstream os; os << "{\"upserted\":" << itemIds.size() << ",\"embedded\":" << embedded << "}"; return os.str();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* recall_context
|
* recall_context
|
||||||
* Request: { "namespace": string, "key?": string, "tags?": string[], "limit?": int, "since?": iso8601 }
|
* Request: { "namespace": string, "key?": string, "tags?": string[], "limit?": int, "since?": iso8601 }
|
||||||
|
|
@ -818,7 +771,7 @@ inline std::string warm_cache(const std::string& reqJson) {
|
||||||
watcher.setFuture(fut); loop.exec(); const KI::KIEmbeddingResult result = watcher.result();
|
watcher.setFuture(fut); loop.exec(); const KI::KIEmbeddingResult result = watcher.result();
|
||||||
|
|
||||||
// Persist
|
// Persist
|
||||||
int persisted = 0; const int n = std::min((size_t)result.vectors.size(), toEmbed.size());
|
int persisted = 0; const int n = std::min(result.vectors.size(), (int)toEmbed.size());
|
||||||
for (int i = 0; i < n; ++i) {
|
for (int i = 0; i < n; ++i) {
|
||||||
const auto &pair = toEmbed[(size_t)i];
|
const auto &pair = toEmbed[(size_t)i];
|
||||||
ki::ChunkRow chunk; chunk.item_id = pair.first; chunk.ord = 0; chunk.text = pair.second;
|
ki::ChunkRow chunk; chunk.item_id = pair.first; chunk.ord = 0; chunk.text = pair.second;
|
||||||
|
|
@ -845,3 +798,49 @@ inline std::string delete_context(const std::string& reqJson) {
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Handlers
|
} // namespace Handlers
|
||||||
|
/**
|
||||||
|
* upsert_and_embed
|
||||||
|
* Request: { namespace, model?, items: [{id?, text, tags?, metadata?}] }
|
||||||
|
* Response: { upserted, embedded }
|
||||||
|
*/
|
||||||
|
inline std::string upsert_and_embed(const std::string& reqJson) {
|
||||||
|
const std::string nsName = detail::extract_string_field(reqJson, "namespace");
|
||||||
|
if (nsName.empty()) return detail::error_response("bad_request","namespace is required");
|
||||||
|
auto nsRow = detail::database().ensureNamespace(nsName);
|
||||||
|
if (!nsRow) return detail::error_response("internal_error","failed to ensure namespace");
|
||||||
|
|
||||||
|
auto items = detail::parse_items(reqJson);
|
||||||
|
if (items.empty()) return detail::error_response("bad_request","items array must contain at least one entry");
|
||||||
|
std::string model = detail::extract_string_field(reqJson, "model");
|
||||||
|
|
||||||
|
// Upsert items first and collect texts/ids
|
||||||
|
std::vector<std::string> itemIds; itemIds.reserve(items.size());
|
||||||
|
std::vector<std::string> texts; texts.reserve(items.size());
|
||||||
|
for (auto &it : items) {
|
||||||
|
ki::ItemRow row; row.id = it.id; row.namespace_id = nsRow->id; row.text = it.text;
|
||||||
|
row.tags = it.tags; row.revision = 1; row.metadata_json = it.metadataJson.empty()?"{}":it.metadataJson; row.content_json = it.rawJson;
|
||||||
|
const std::string id = detail::database().upsertItem(row);
|
||||||
|
itemIds.push_back(id); texts.push_back(it.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embed via libKI
|
||||||
|
KI::KIClient client; KI::OllamaProvider provider; client.setProvider(&provider);
|
||||||
|
KI::KIEmbedOptions opts; if (!model.empty()) opts.model = QString::fromStdString(model);
|
||||||
|
QStringList qtexts; for (auto &t : texts) qtexts.push_back(QString::fromStdString(t));
|
||||||
|
QEventLoop loop; QFuture<KI::KIEmbeddingResult> fut = client.embed(qtexts, opts);
|
||||||
|
QFutureWatcher<KI::KIEmbeddingResult> watcher; QObject::connect(&watcher, &QFutureWatcher<KI::KIEmbeddingResult>::finished, &loop, &QEventLoop::quit); watcher.setFuture(fut); loop.exec();
|
||||||
|
const KI::KIEmbeddingResult result = watcher.result();
|
||||||
|
|
||||||
|
// Upsert chunks + embeddings (ord=0)
|
||||||
|
int embedded = 0;
|
||||||
|
const int n = std::min((int)itemIds.size(), result.vectors.size());
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
ki::ChunkRow chunk; chunk.item_id = itemIds[(size_t)i]; chunk.ord = 0; chunk.text = texts[(size_t)i];
|
||||||
|
auto chunkIds = detail::database().upsertChunks(std::vector<ki::ChunkRow>{chunk}); if (chunkIds.empty()) continue;
|
||||||
|
ki::EmbeddingRow emb; emb.chunk_id = chunkIds.front(); emb.model = result.model.toStdString(); emb.dim = result.vectors[i].size();
|
||||||
|
emb.vector.assign(result.vectors[i].begin(), result.vectors[i].end());
|
||||||
|
detail::database().upsertEmbeddings(std::vector<ki::EmbeddingRow>{emb}); embedded++;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostringstream os; os << "{\"upserted\":" << itemIds.size() << ",\"embedded\":" << embedded << "}"; return os.str();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,3 @@ qt_add_executable(test_snapshot
|
||||||
)
|
)
|
||||||
target_link_libraries(test_snapshot PRIVATE Qt6::Core Qt6::Network Qt6::Test kompanion_mw)
|
target_link_libraries(test_snapshot PRIVATE Qt6::Core Qt6::Network Qt6::Test kompanion_mw)
|
||||||
add_test(NAME test_snapshot COMMAND test_snapshot)
|
add_test(NAME test_snapshot COMMAND test_snapshot)
|
||||||
|
|
||||||
add_test(NAME cli_smoke
|
|
||||||
COMMAND sh ${CMAKE_SOURCE_DIR}/tests/cli_smoke.sh $<TARGET_FILE:kompanion>)
|
|
||||||
set_tests_properties(cli_smoke PROPERTIES ENVIRONMENT "KOMPANION_SKIP_CLI_SMOKE=1")
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue