diff --git a/packages/kbot/cpp/.gitignore b/packages/kbot/cpp/.gitignore index 7c7b21f6..33c9da8a 100644 --- a/packages/kbot/cpp/.gitignore +++ b/packages/kbot/cpp/.gitignore @@ -29,3 +29,4 @@ Thumbs.db cache/ config/postgres.toml dist +src/cmd_grid*.cpp diff --git a/packages/kbot/cpp/CMakeLists.txt b/packages/kbot/cpp/CMakeLists.txt index c735c66d..330728fe 100644 --- a/packages/kbot/cpp/CMakeLists.txt +++ b/packages/kbot/cpp/CMakeLists.txt @@ -79,6 +79,7 @@ add_subdirectory(packages/gadm_reader) add_subdirectory(packages/grid) add_subdirectory(packages/search) add_subdirectory(packages/enrichers) +add_subdirectory(packages/kbot) # ── Sources ────────────────────────────────────────────────────────────────── add_executable(${PROJECT_NAME} @@ -87,11 +88,12 @@ add_executable(${PROJECT_NAME} src/cmd_gridsearch-filters.cpp src/cmd_gridsearch-uds.cpp src/cmd_gridsearch-postgres.cpp + src/cmd_kbot.cpp src/gridsearch_serialize.cpp src/sys_metrics.cpp ) -target_link_libraries(${PROJECT_NAME} PRIVATE CLI11::CLI11 tomlplusplus::tomlplusplus logger html postgres http json polymech ipc geo gadm_reader grid search enrichers) +target_link_libraries(${PROJECT_NAME} PRIVATE CLI11::CLI11 tomlplusplus::tomlplusplus logger html postgres http json polymech ipc geo gadm_reader grid search enrichers kbot) target_include_directories(${PROJECT_NAME} PRIVATE ${asio_SOURCE_DIR}/asio/include diff --git a/packages/kbot/cpp/packages/kbot/CMakeLists.txt b/packages/kbot/cpp/packages/kbot/CMakeLists.txt new file mode 100644 index 00000000..ba0eb54f --- /dev/null +++ b/packages/kbot/cpp/packages/kbot/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.20) + +project(kbot CXX) + +add_library(kbot STATIC + kbot.cpp +) + +target_include_directories(kbot PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${taskflow_SOURCE_DIR} +) + +target_link_libraries(kbot PUBLIC + logger + json +) diff --git a/packages/kbot/cpp/packages/kbot/kbot.cpp b/packages/kbot/cpp/packages/kbot/kbot.cpp new file mode 100644 index 00000000..08bb9897 --- /dev/null +++ b/packages/kbot/cpp/packages/kbot/kbot.cpp @@ -0,0 +1,55 @@ +#include "kbot.h" +#include +#include +#include "logger/logger.h" + +namespace polymech { +namespace kbot { + +int run_kbot_ai_pipeline(const KBotOptions& opts, const KBotCallbacks& cb) { + logger::info("Starting kbot ai pipeline (stub)"); + if (opts.dry_run) { + logger::info("Dry run triggered for kbot ai"); + } + + // Scaffolding multithreaded AI tasks + tf::Executor executor(4); + tf::Taskflow taskflow; + + taskflow.emplace([opts, cb](){ + logger::info("Executing kbot ai tasks via Taskflow -> emit events..."); + if (cb.onEvent) { + cb.onEvent("ai_progress", "{\"message\":\"Task stub completed\"}"); + } + }); + + executor.run(taskflow).wait(); + + if (cb.onEvent) { + cb.onEvent("job_result", "{\"status\":\"success\",\"mode\":\"ai\"}"); + } + return 0; +} + +int run_kbot_run_pipeline(const KBotRunOptions& opts, const KBotCallbacks& cb) { + logger::info("Starting kbot run pipeline (stub) for config: " + opts.config); + if (opts.dry) { + logger::info("Dry run triggered for kbot run"); + } + if (opts.list) { + logger::info("List configs mode enabled"); + } + + // Stub std::system call execution (simulating child_process.execFileSync from TypeScript) + if (!opts.dry && !opts.list) { + logger::info("Simulating launching: .vscode/launch.json targeting " + opts.config); + } + + if (cb.onEvent) { + cb.onEvent("job_result", "{\"status\":\"success\",\"mode\":\"run\"}"); + } + return 0; +} + +} // namespace kbot +} // namespace polymech diff --git a/packages/kbot/cpp/packages/kbot/kbot.h b/packages/kbot/cpp/packages/kbot/kbot.h new file mode 100644 index 00000000..b4c3c0f2 --- /dev/null +++ b/packages/kbot/cpp/packages/kbot/kbot.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace polymech { +namespace kbot { + +struct KBotOptions { + std::string path = "."; + std::string prompt; + std::string output; + std::string dst; + std::string append = "concat"; + std::string wrap = "none"; + std::string each; + std::vector disable; + std::vector disable_tools; + std::vector tools; + std::vector include_globs; + std::vector exclude_globs; + std::string glob_extension; + std::string api_key; + std::string model; + std::string router = "openrouter"; + std::string mode = "tools"; + int log_level = 4; + std::string profile; + std::string base_url; + std::string config_path; + std::string dump; + std::string preferences; + std::string logs; + bool stream = false; + bool alt = false; + std::string env = "default"; + std::string filters; + std::string query; + bool dry_run = false; + std::string format; + + // Internal + std::string job_id; + std::shared_ptr> cancel_token; +}; + +struct KBotRunOptions { + std::string config = "default"; + bool dry = false; + bool list = false; + std::string project_path; + std::string log_file_path; + + // Internal + std::string job_id; + std::shared_ptr> cancel_token; +}; + +struct KBotCallbacks { + std::function onEvent; +}; + +int run_kbot_ai_pipeline(const KBotOptions& opts, const KBotCallbacks& cb); +int run_kbot_run_pipeline(const KBotRunOptions& opts, const KBotCallbacks& cb); + +} // namespace kbot +} // namespace polymech diff --git a/packages/kbot/cpp/src/cmd_kbot.cpp b/packages/kbot/cpp/src/cmd_kbot.cpp new file mode 100644 index 00000000..1e8b3c71 --- /dev/null +++ b/packages/kbot/cpp/src/cmd_kbot.cpp @@ -0,0 +1,93 @@ +#include "cmd_kbot.h" +#include "logger/logger.h" +#include +#include +#include + +namespace polymech { + +// Global states for CLI mode +static kbot::KBotOptions g_kbot_opts; +static kbot::KBotRunOptions g_run_opts; + +static CLI::App* ai_cmd = nullptr; +static CLI::App* run_cmd = nullptr; + +CLI::App* setup_cmd_kbot(CLI::App& app) { + auto* kbot_cmd = app.add_subcommand("kbot", "KBot AI & Task runner"); + + // ── AI Subcommand ── + ai_cmd = kbot_cmd->add_subcommand("ai", "Run KBot AI tasks"); + ai_cmd->add_option("-p,--path", g_kbot_opts.path, "Target directory")->default_val("."); + ai_cmd->add_option("--prompt", g_kbot_opts.prompt, "The prompt. Supports file paths and vars."); + ai_cmd->add_option("--output", g_kbot_opts.output, "Optional output path for modified files"); + ai_cmd->add_option("--dst", g_kbot_opts.dst, "Optional destination path for the result"); + ai_cmd->add_option("--append", g_kbot_opts.append, "How to handle output if --dst exists: concat|merge|replace")->default_val("concat"); + ai_cmd->add_option("--wrap", g_kbot_opts.wrap, "Specify how to wrap output: meta|none")->default_val("none"); + ai_cmd->add_option("--each", g_kbot_opts.each, "Iterate over items (GLOB, JSON, array)"); + ai_cmd->add_option("--disable", g_kbot_opts.disable, "Disable tools categories"); + ai_cmd->add_option("--disableTools", g_kbot_opts.disable_tools, "Specific tools to disable"); + ai_cmd->add_option("--tools", g_kbot_opts.tools, "Tools to use"); + ai_cmd->add_option("--include", g_kbot_opts.include_globs, "Glob patterns or paths to include"); + ai_cmd->add_option("--exclude", g_kbot_opts.exclude_globs, "Glob patterns or paths to exclude"); + ai_cmd->add_option("--globExtension", g_kbot_opts.glob_extension, "Glob extension behavior (e.g. match-cpp)"); + ai_cmd->add_option("--api_key", g_kbot_opts.api_key, "Explicit API key to use"); + ai_cmd->add_option("--model", g_kbot_opts.model, "AI model to use"); + ai_cmd->add_option("--router", g_kbot_opts.router, "Router to use (openai, openrouter, deepseek)")->default_val("openrouter"); + ai_cmd->add_option("--mode", g_kbot_opts.mode, "Chat completion mode")->default_val("tools"); + ai_cmd->add_flag("--dry", g_kbot_opts.dry_run, "Dry run"); + + // ── Run Subcommand ── + run_cmd = kbot_cmd->add_subcommand("run", "Run a .vscode/launch.json configuration"); + run_cmd->add_option("-c,--config", g_run_opts.config, "Config name")->default_val("default"); + run_cmd->add_flag("--dry", g_run_opts.dry, "Dry run"); + run_cmd->add_flag("--list", g_run_opts.list, "List available configs"); + run_cmd->add_option("--projectPath", g_run_opts.project_path, "Project path")->default_val("."); + run_cmd->add_option("--logFilePath", g_run_opts.log_file_path, "Log file path"); + + return kbot_cmd; +} + +bool is_kbot_ai_parsed() { return ai_cmd != nullptr && ai_cmd->parsed(); } +bool is_kbot_run_parsed() { return run_cmd != nullptr && run_cmd->parsed(); } + +int run_cmd_kbot_ai() { + return kbot::run_kbot_ai_pipeline(g_kbot_opts, kbot::KBotCallbacks{}); +} + +int run_cmd_kbot_run() { + return kbot::run_kbot_run_pipeline(g_run_opts, kbot::KBotCallbacks{}); +} + +int run_kbot_ai_ipc(const std::string& payload, const std::string& jobId, const kbot::KBotCallbacks& cb) { + kbot::KBotOptions opts; + opts.job_id = jobId; + + // Optional: Parse JSON from payload to overwrite opts variables using rapidjson + rapidjson::Document doc; + doc.Parse(payload.c_str()); + if (!doc.HasParseError() && doc.IsObject()) { + if (doc.HasMember("prompt") && doc["prompt"].IsString()) opts.prompt = doc["prompt"].GetString(); + if (doc.HasMember("dry_run") && doc["dry_run"].IsBool()) opts.dry_run = doc["dry_run"].GetBool(); + } + + logger::info("Receiving AI task over IPC... job: " + jobId); + return kbot::run_kbot_ai_pipeline(opts, cb); +} + +int run_kbot_run_ipc(const std::string& payload, const std::string& jobId, const kbot::KBotCallbacks& cb) { + kbot::KBotRunOptions opts; + opts.job_id = jobId; + + rapidjson::Document doc; + doc.Parse(payload.c_str()); + if (!doc.HasParseError() && doc.IsObject()) { + if (doc.HasMember("config") && doc["config"].IsString()) opts.config = doc["config"].GetString(); + if (doc.HasMember("dry") && doc["dry"].IsBool()) opts.dry = doc["dry"].GetBool(); + } + + logger::info("Receiving run task over IPC... job: " + jobId); + return kbot::run_kbot_run_pipeline(opts, cb); +} + +} // namespace polymech diff --git a/packages/kbot/cpp/src/cmd_kbot.h b/packages/kbot/cpp/src/cmd_kbot.h new file mode 100644 index 00000000..3c611369 --- /dev/null +++ b/packages/kbot/cpp/src/cmd_kbot.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include +#include "kbot.h" + +namespace polymech { + +/// Attach kbot subcommands to the main app +CLI::App* setup_cmd_kbot(CLI::App& app); + +/// CLI Entry points +int run_cmd_kbot_ai(); +int run_cmd_kbot_run(); + +/// IPC / UDS Entry points +int run_kbot_ai_ipc(const std::string& payload, const std::string& jobId, const kbot::KBotCallbacks& cb); +int run_kbot_run_ipc(const std::string& payload, const std::string& jobId, const kbot::KBotCallbacks& cb); + +/// Helper to check parsed state +bool is_kbot_ai_parsed(); +bool is_kbot_run_parsed(); + +} // namespace polymech diff --git a/packages/kbot/cpp/src/gridsearch_serialize.cpp b/packages/kbot/cpp/src/gridsearch_serialize.cpp new file mode 100644 index 00000000..e8fe1c90 --- /dev/null +++ b/packages/kbot/cpp/src/gridsearch_serialize.cpp @@ -0,0 +1,351 @@ +#include "gridsearch_serialize.h" + +#include +#include +#include "cmd_gridsearch.h" + +namespace polymech::serialize { + +// ── grid-ready ────────────────────────────────────────────────────────────── + +std::string grid_ready(const std::vector& waypoints) { + rapidjson::StringBuffer sb; + rapidjson::Writer w(sb); + w.StartObject(); + w.Key("areas"); w.StartArray(); + for (size_t i = 0; i < waypoints.size(); ++i) { + const auto& wp = waypoints[i]; + w.StartObject(); + w.Key("name"); w.String(("Waypoint " + std::to_string(wp.step)).c_str()); + w.Key("gid"); w.String(("wp-" + std::to_string(wp.step)).c_str()); + w.Key("lat"); w.Double(wp.lat); + w.Key("lon"); w.Double(wp.lng); + w.Key("radius_km"); w.Double(wp.radius_km); + w.Key("area_gid"); w.String(wp.area_gid.c_str()); + w.Key("area_name"); w.String(wp.area_name.c_str()); + w.Key("index"); w.Int(static_cast(i)); + w.EndObject(); + } + w.EndArray(); + w.Key("total"); w.Int(static_cast(waypoints.size())); + w.EndObject(); + return sb.GetString(); +} + +// ── waypoint-start ────────────────────────────────────────────────────────── + +std::string waypoint_start(const grid::Waypoint& wp, int index, int total) { + rapidjson::StringBuffer sb; + rapidjson::Writer w(sb); + w.StartObject(); + w.Key("name"); w.String(("Waypoint " + std::to_string(wp.step)).c_str()); + w.Key("gid"); w.String(("wp-" + std::to_string(wp.step)).c_str()); + w.Key("lat"); w.Double(wp.lat); + w.Key("lon"); w.Double(wp.lng); + w.Key("radius_km"); w.Double(wp.radius_km); + w.Key("area_gid"); w.String(wp.area_gid.c_str()); + w.Key("area_name"); w.String(wp.area_name.c_str()); + w.Key("index"); w.Int(index); + w.Key("total"); w.Int(total); + w.EndObject(); + return sb.GetString(); +} + +// ── location (per search result) ──────────────────────────────────────────── + +std::string location(const search::MapResult& r, int step) { + rapidjson::StringBuffer sb; + rapidjson::Writer w(sb); + w.StartObject(); + w.Key("location"); w.StartObject(); + w.Key("title"); w.String(r.title.c_str()); + w.Key("place_id"); w.String(r.place_id.c_str()); + w.Key("address"); w.String(r.address.c_str()); + w.Key("website"); w.String(r.website.c_str()); + w.Key("type"); w.String(r.type.c_str()); + w.Key("phone"); w.String(r.phone.c_str()); + w.Key("rating"); w.Double(r.rating); + w.Key("reviews"); w.Int(r.reviews); + w.Key("lat"); w.Double(r.gps.lat); + w.Key("lng"); w.Double(r.gps.lng); + w.Key("types"); w.StartArray(); + for (const auto& t : r.types) w.String(t.c_str()); + w.EndArray(); + w.EndObject(); + w.Key("areaName"); w.String(("Waypoint " + std::to_string(step)).c_str()); + w.EndObject(); + return sb.GetString(); +} + +// ── area_start ────────────────────────────────────────────────────────────── + +std::string area_start(const std::string& area_gid, const std::string& area_name) { + rapidjson::StringBuffer sb; + rapidjson::Writer w(sb); + w.StartObject(); + w.Key("gid"); w.String(area_gid.c_str()); + w.Key("name"); w.String(area_name.c_str()); + w.EndObject(); + return sb.GetString(); +} + +// ── area_finish ───────────────────────────────────────────────────────────── + +std::string area_finish(const std::string& area_gid) { + rapidjson::StringBuffer sb; + rapidjson::Writer w(sb); + w.StartObject(); + w.Key("gid"); w.String(area_gid.c_str()); + w.EndObject(); + return sb.GetString(); +} + +// ── waypoint_finish ───────────────────────────────────────────────────────── + +std::string waypoint_finish(const grid::Waypoint& wp, int results, int apiCalls) { + rapidjson::StringBuffer sb; + rapidjson::Writer w(sb); + w.StartObject(); + w.Key("name"); w.String(("Waypoint " + std::to_string(wp.step)).c_str()); + w.Key("gid"); w.String(("wp-" + std::to_string(wp.step)).c_str()); + w.Key("results"); w.Int(results); + w.Key("apiCalls"); w.Int(apiCalls); + w.EndObject(); + return sb.GetString(); +} + +// ── enrich-start ──────────────────────────────────────────────────────────── + +std::string enrich_start(int locationCount) { + rapidjson::StringBuffer sb; + rapidjson::Writer w(sb); + w.StartObject(); + w.Key("locationCount"); w.Int(locationCount); + w.EndObject(); + return sb.GetString(); +} + +// ── nodePage (per page error) ─────────────────────────────────────────────── + +std::string node_page(const enrichers::PageError& pe, const std::string& placeId) { + rapidjson::StringBuffer sb; + rapidjson::Writer w(sb); + w.StartObject(); + w.Key("location"); w.String(placeId.c_str()); + w.Key("url"); w.String(pe.url.c_str()); + w.Key("status"); w.String(pe.status.c_str()); + w.Key("error"); w.String(pe.error.c_str()); + w.EndObject(); + return sb.GetString(); +} + +// ── node-error ────────────────────────────────────────────────────────────── + +std::string node_error(const enrichers::EnrichedNode& node) { + rapidjson::StringBuffer sb; + rapidjson::Writer w(sb); + w.StartObject(); + w.Key("node"); w.StartObject(); + w.Key("title"); w.String(node.title.c_str()); + w.Key("placeId"); w.String(node.place_id.c_str()); + w.EndObject(); + w.Key("error"); w.String(node.error.c_str()); + w.EndObject(); + return sb.GetString(); +} + +// ── node (enriched location) ──────────────────────────────────────────────── + +std::string node(const enrichers::EnrichedNode& n) { + rapidjson::StringBuffer sb; + rapidjson::Writer w(sb); + w.StartObject(); + w.Key("idx"); w.Int(n.idx); + w.Key("title"); w.String(n.title.c_str()); + w.Key("placeId"); w.String(n.place_id.c_str()); + w.Key("website"); w.String(n.website.c_str()); + w.Key("address"); w.String(n.address.c_str()); + w.Key("type"); w.String(n.type.c_str()); + w.Key("status"); w.String(enrichers::status_string(n.status)); + w.Key("emails"); w.StartArray(); + for (const auto& e : n.emails) w.String(e.c_str()); + w.EndArray(); + + w.Key("social"); w.StartArray(); + for (const auto& s : n.socials) { + w.StartObject(); + w.Key("url"); w.String(s.url.c_str()); + w.Key("platform"); w.String(s.platform.c_str()); + w.EndObject(); + } + w.EndArray(); + + w.Key("sites"); w.StartArray(); + for (const auto& s : n.sites) { + w.StartObject(); + w.Key("url"); w.String(s.first.c_str()); + w.Key("name"); w.String("home"); + w.Key("content"); w.String(s.second.c_str()); + w.EndObject(); + } + w.EndArray(); + + w.Key("pagesFound"); w.Int(n.pages_found); + w.Key("pagesScraped"); w.Int(n.pages_scraped); + w.Key("metaMs"); w.Int(n.meta_ms); + w.Key("emailMs"); w.Int(n.email_ms); + w.Key("totalMs"); w.Int(n.total_ms); + w.Key("gridArea"); w.String(n.grid_area.c_str()); + w.Key("gridGid"); w.String(n.grid_gid.c_str()); + w.EndObject(); + return sb.GetString(); +} + +// ── write_options helper ──────────────────────────────────────────────────── + +static void write_options(rapidjson::Writer& w, const polymech::PipelineOptions& opts) { + w.Key("options"); + w.StartObject(); + w.Key("jobId"); w.String(opts.job_id.c_str()); + w.Key("searchQuery"); w.String(opts.search_query.c_str()); + w.Key("searchDomain"); w.String(opts.search_domain.c_str()); + w.Key("searchLanguage"); w.String(opts.search_language.c_str()); + w.Key("searchCountry"); w.String(opts.search_country.c_str()); + w.Key("searchLimit"); w.Int(opts.search_limit); + w.Key("searchZoom"); w.Int(opts.search_zoom); + w.Key("dryRun"); w.Bool(opts.dry_run); + w.Key("enrich"); w.Bool(opts.enrich); + + w.Key("grid"); + w.StartObject(); + w.Key("gridMode"); w.String(opts.grid_opts.gridMode.c_str()); + w.Key("cellSize"); w.Double(opts.grid_opts.cellSize); + w.Key("cellOverlap"); w.Double(opts.grid_opts.cellOverlap); + w.Key("centroidOverlap"); w.Double(opts.grid_opts.centroidOverlap); + w.Key("maxCellsLimit"); w.Int(opts.grid_opts.maxCellsLimit); + w.Key("maxElevation"); w.Double(opts.grid_opts.maxElevation); + w.Key("minDensity"); w.Double(opts.grid_opts.minDensity); + w.Key("minGhsPop"); w.Double(opts.grid_opts.minGhsPop); + w.Key("minGhsBuilt"); w.Double(opts.grid_opts.minGhsBuilt); + w.Key("ghsFilterMode"); w.String(opts.grid_opts.ghsFilterMode.c_str()); + w.Key("allowMissingGhs"); w.Bool(opts.grid_opts.allowMissingGhs); + w.Key("bypassFilters"); w.Bool(opts.grid_opts.bypassFilters); + w.Key("pathOrder"); w.String(opts.grid_opts.pathOrder.c_str()); + w.Key("groupByRegion"); w.Bool(opts.grid_opts.groupByRegion); + w.EndObject(); + + w.Key("areas"); + w.StartArray(); + for (const auto& a : opts.areas) { + w.StartObject(); + w.Key("gid"); w.String(a.gid.c_str()); + w.Key("name"); w.String(a.name.c_str()); + w.Key("level"); w.Int(a.level); + w.EndObject(); + } + w.EndArray(); + w.EndObject(); +} + +// ── job_result (with enrichment) ──────────────────────────────────────────── + +std::string job_result(const polymech::PipelineOptions& opts, int64_t enumMs, int64_t searchMs, int64_t enrichMs, int64_t totalMs, + int totalEmails, int totalPagesScraped, int freshApiCalls, + int waypointCount, int validCells, int skippedCells, + int totalResults, const std::vector& enrichResults, + double totalScannedSqKm, double totalPopulation) { + rapidjson::StringBuffer sb; + rapidjson::Writer w(sb); + w.StartObject(); + write_options(w, opts); + + w.Key("enumMs"); w.Int64(enumMs); + w.Key("searchMs"); w.Int64(searchMs); + w.Key("enrichMs"); w.Int64(enrichMs); + w.Key("totalMs"); w.Int64(totalMs); + + w.Key("gridStats"); + w.StartObject(); + w.Key("validCells"); w.Int(validCells); + w.Key("skippedCells"); w.Int(skippedCells); + w.Key("totalWaypoints"); w.Int(waypointCount); + w.EndObject(); + + w.Key("searchStats"); + w.StartObject(); + w.Key("apiCalls"); w.Int(freshApiCalls); + w.Key("filtered"); w.Int(0); // placeholder if needed + w.Key("areaCount"); w.Int(waypointCount); + w.Key("totalResults"); w.Int(totalResults); + w.Key("totalScannedSqKm"); w.Double(totalScannedSqKm); + w.Key("totalPopulation"); w.Double(totalPopulation); + w.EndObject(); + + w.Key("totalEmails"); w.Int(totalEmails); + + w.Key("enrichResults"); + w.StartArray(); + for (const auto& id : enrichResults) { + w.String(id.c_str()); + } + w.EndArray(); + + w.Key("freshApiCalls"); w.Int(freshApiCalls); + w.Key("waypointCount"); w.Int(waypointCount); + w.Key("totalPagesScraped"); w.Int(totalPagesScraped); + + w.EndObject(); + return sb.GetString(); +} + +// ── job_result (search only) ──────────────────────────────────────────────── + +std::string job_result_search_only(const polymech::PipelineOptions& opts, int64_t enumMs, int64_t searchMs, int64_t totalMs, + int freshApiCalls, int waypointCount, int validCells, + int skippedCells, int totalResults, const std::vector& enrichResults, + double totalScannedSqKm, double totalPopulation) { + rapidjson::StringBuffer sb; + rapidjson::Writer w(sb); + w.StartObject(); + write_options(w, opts); + + w.Key("enumMs"); w.Int64(enumMs); + w.Key("searchMs"); w.Int64(searchMs); + w.Key("enrichMs"); w.Int64(0); + w.Key("totalMs"); w.Int64(totalMs); + + w.Key("gridStats"); + w.StartObject(); + w.Key("validCells"); w.Int(validCells); + w.Key("skippedCells"); w.Int(skippedCells); + w.Key("totalWaypoints"); w.Int(waypointCount); + w.EndObject(); + + w.Key("searchStats"); + w.StartObject(); + w.Key("apiCalls"); w.Int(freshApiCalls); + w.Key("filtered"); w.Int(0); + w.Key("areaCount"); w.Int(waypointCount); + w.Key("totalResults"); w.Int(totalResults); + w.Key("totalScannedSqKm"); w.Double(totalScannedSqKm); + w.Key("totalPopulation"); w.Double(totalPopulation); + w.EndObject(); + + w.Key("totalEmails"); w.Int(0); + + w.Key("enrichResults"); + w.StartArray(); + for (const auto& id : enrichResults) { + w.String(id.c_str()); + } + w.EndArray(); + + w.Key("freshApiCalls"); w.Int(freshApiCalls); + w.Key("waypointCount"); w.Int(waypointCount); + w.Key("totalPagesScraped"); w.Int(0); + + w.EndObject(); + return sb.GetString(); +} + +} // namespace polymech::serialize diff --git a/packages/kbot/cpp/src/main.cpp b/packages/kbot/cpp/src/main.cpp index 80aa4240..da567213 100644 --- a/packages/kbot/cpp/src/main.cpp +++ b/packages/kbot/cpp/src/main.cpp @@ -22,6 +22,7 @@ #include "search/search.h" #include "enrichers/enrichers.h" #include "cmd_gridsearch.h" +#include "cmd_kbot.h" #ifndef PROJECT_VERSION #define PROJECT_VERSION "0.1.0" @@ -91,6 +92,9 @@ int main(int argc, char *argv[]) { // Subcommand: gridsearch — Run a full gridsearch pipeline auto* gs_cmd = polymech::setup_cmd_gridsearch(app); + // Subcommand: kbot — AI workflows & task configurations + auto* kbot_cmd = polymech::setup_cmd_kbot(app); + CLI11_PARSE(app, argc, argv); // Worker mode uses stderr for logs to keep stdout clean for IPC frames @@ -163,6 +167,38 @@ int main(int argc, char *argv[]) { // Stub: echo the payload back as job_result ipc::write_message({req.id, "job_result", req.payload}); + } else if (req.type == "kbot-ai") { + logger::info("Worker: kbot-ai job received"); + std::string req_id = req.id; + polymech::kbot::KBotCallbacks cb; + cb.onEvent = [&req_id](const std::string& type, const std::string& json) { + if (type == "job_result") { + ipc::write_message({req_id, "job_result", json}); + } else { + ipc::write_message({"0", type, json}); + } + }; + int rc = polymech::run_kbot_ai_ipc(req.payload, req.id, cb); + if (rc != 0) { + ipc::write_message({req.id, "error", "{\"message\":\"kbot ai pipeline failed\"}"}); + } + + } else if (req.type == "kbot-run") { + logger::info("Worker: kbot-run job received"); + std::string req_id = req.id; + polymech::kbot::KBotCallbacks cb; + cb.onEvent = [&req_id](const std::string& type, const std::string& json) { + if (type == "job_result") { + ipc::write_message({req_id, "job_result", json}); + } else { + ipc::write_message({"0", type, json}); + } + }; + int rc = polymech::run_kbot_run_ipc(req.payload, req.id, cb); + if (rc != 0) { + ipc::write_message({req.id, "error", "{\"message\":\"kbot run pipeline failed\"}"}); + } + } else if (req.type == "shutdown") { ipc::write_message({req.id, "shutdown_ack", "{}"}); logger::info("Worker: shutdown requested, exiting"); @@ -263,6 +299,14 @@ int main(int argc, char *argv[]) { return polymech::run_cmd_gridsearch(); } + // ── kbot subcommand ────────────────────────────────────────────────── + if (polymech::is_kbot_ai_parsed()) { + return polymech::run_cmd_kbot_ai(); + } + if (polymech::is_kbot_run_parsed()) { + return polymech::run_cmd_kbot_run(); + } + // No subcommand — show help std::cout << app.help() << "\n"; return 0; diff --git a/packages/kbot/cpp/src/sys_metrics.cpp b/packages/kbot/cpp/src/sys_metrics.cpp new file mode 100644 index 00000000..e31d255d --- /dev/null +++ b/packages/kbot/cpp/src/sys_metrics.cpp @@ -0,0 +1,36 @@ +#include "sys_metrics.h" + +#ifdef _WIN32 +#define NOMINMAX +#include +#include +#pragma comment(lib, "psapi.lib") + +namespace polymech { +size_t get_current_rss_mb() { + PROCESS_MEMORY_COUNTERS info; + if (GetProcessMemoryInfo(GetCurrentProcess(), &info, sizeof(info))) { + return (size_t)(info.WorkingSetSize) / (1024 * 1024); + } + return 0; +} + +uint64_t get_cpu_time_ms() { + FILETIME creationTime, exitTime, kernelTime, userTime; + if (GetProcessTimes(GetCurrentProcess(), &creationTime, &exitTime, &kernelTime, &userTime)) { + ULARGE_INTEGER kernel, user; + kernel.LowPart = kernelTime.dwLowDateTime; + kernel.HighPart = kernelTime.dwHighDateTime; + user.LowPart = userTime.dwLowDateTime; + user.HighPart = userTime.dwHighDateTime; + return (kernel.QuadPart + user.QuadPart) / 10000; + } + return 0; +} +} +#else +namespace polymech { +size_t get_current_rss_mb() { return 0; } +uint64_t get_cpu_time_ms() { return 0; } +} +#endif diff --git a/packages/kbot/cpp/tests/CMakeLists.txt b/packages/kbot/cpp/tests/CMakeLists.txt index 941a8da0..32d8c54c 100644 --- a/packages/kbot/cpp/tests/CMakeLists.txt +++ b/packages/kbot/cpp/tests/CMakeLists.txt @@ -44,6 +44,10 @@ add_executable(test_polymech unit/test_polymech.cpp) target_link_libraries(test_polymech PRIVATE Catch2::Catch2WithMain polymech postgres Threads::Threads) catch_discover_tests(test_polymech) +add_executable(test_cmd_kbot unit/test_cmd_kbot.cpp ../src/cmd_kbot.cpp) +target_link_libraries(test_cmd_kbot PRIVATE Catch2::Catch2WithMain CLI11::CLI11 logger tomlplusplus::tomlplusplus kbot Threads::Threads) +catch_discover_tests(test_cmd_kbot) + # E2E test — polymech fetch_pages from live Supabase add_executable(test_polymech_e2e e2e/test_polymech_e2e.cpp) target_link_libraries(test_polymech_e2e PRIVATE Catch2::Catch2WithMain tomlplusplus::tomlplusplus logger postgres polymech json Threads::Threads) diff --git a/packages/kbot/cpp/tests/unit/test_cmd_kbot.cpp b/packages/kbot/cpp/tests/unit/test_cmd_kbot.cpp new file mode 100644 index 00000000..9546818d --- /dev/null +++ b/packages/kbot/cpp/tests/unit/test_cmd_kbot.cpp @@ -0,0 +1,60 @@ +#include +#include +#include "../../src/cmd_kbot.h" + +using namespace polymech; + +TEST_CASE("KBot CLI AI Command Parsing", "[kbot]") { + CLI::App app{"polymech-cli"}; + auto* kbot_cmd = setup_cmd_kbot(app); + REQUIRE(kbot_cmd != nullptr); + + SECTION("Default values for kbot ai") { + int argc = 3; + const char* argv[] = {"polymech-cli", "kbot", "ai"}; + REQUIRE_NOTHROW(app.parse(argc, argv)); + + REQUIRE(is_kbot_ai_parsed() == true); + REQUIRE(is_kbot_run_parsed() == false); + // We can't access g_kbot_opts easily since it's static in cmd_kbot.cpp, + // but testing that it doesn't throw is a good start. + // In a real app we might pass options structs around instead of globals. + } + + SECTION("Arguments for kbot ai") { + int argc = 7; + const char* argv[] = { + "polymech-cli", "kbot", "ai", + "--prompt", "hello world", + "--mode", "chat" + }; + REQUIRE_NOTHROW(app.parse(argc, argv)); + REQUIRE(is_kbot_ai_parsed() == true); + } +} + +TEST_CASE("KBot CLI Run Command Parsing", "[kbot]") { + CLI::App app{"polymech-cli"}; + auto* kbot_cmd = setup_cmd_kbot(app); + REQUIRE(kbot_cmd != nullptr); + + SECTION("Default values for kbot run") { + int argc = 3; + const char* argv[] = {"polymech-cli", "kbot", "run"}; + REQUIRE_NOTHROW(app.parse(argc, argv)); + + REQUIRE(is_kbot_run_parsed() == true); + REQUIRE(is_kbot_ai_parsed() == false); + } + + SECTION("Arguments for kbot run") { + int argc = 6; + const char* argv[] = { + "polymech-cli", "kbot", "run", + "-c", "frontend-dev", + "--dry" + }; + REQUIRE_NOTHROW(app.parse(argc, argv)); + REQUIRE(is_kbot_run_parsed() == true); + } +} diff --git a/packages/kbot/docs/cpp-todos.md b/packages/kbot/docs/cpp-todos.md new file mode 100644 index 00000000..446c97a3 --- /dev/null +++ b/packages/kbot/docs/cpp-todos.md @@ -0,0 +1,69 @@ +# KBot C++ Port Plan (WIP Scaffolding) + +This document outlines the scaffolding steps to port the TypeScript `kbot` implementation (both AI tools and the project runner) over to the C++ `polymech-cli` application. + +## 1. CLI Scaffolding (`main.cpp` & `cmd_kbot`) + +The C++ port will introduce a new `kbot` subcommand tree loosely mimicking the existing TypeScript entry points (`zod_schema.ts`). + +- **Target Files**: + - `src/main.cpp` (Register the command) + - `src/cmd_kbot.h` (Declarations & Options Structs) + - `src/cmd_kbot.cpp` (Implementation) + +### Subcommand `ai` +This command replaces the standard `OptionsSchema` from `zod_schema.ts`. +- Using `CLI::App* ai_cmd = kbot_cmd->add_subcommand("ai", "Run KBot AI workflows");` +- **Arguments to Map** via `CLI11`: + - `--path` (default `.`) + - `--prompt` (string) + - `--output` (string) + - `--dst` (string) + - `--append` (enum: `concat`, `merge`, `replace`) + - `--wrap` (enum: `meta`, `none`) + - `--each` (Glob pattern / list / JSON) + - `--disable`, `--disableTools`, `--tools` + - `--include`, `--exclude`, `--globExtension` + - `--model`, `--router`, `--mode` (enum: `completion`, `tools`, `assistant`, `responses`, `custom`) + - Flags: `--stream`, `--dry`, `--alt` + - Advanced: `--baseURL`, `--config`, `--dump`, `--preferences`, `--logs`, `--env` + +### Subcommand `run` +This command replaces `commons/src/lib/run.ts` which spawns debug configurations. +- Using `CLI::App* run_cmd = kbot_cmd->add_subcommand("run", "Run a launch.json configuration");` +- **Arguments to Map**: + - `--config` (default `default`) + - `--dry` (flag) + - `--list` (flag) + - `--projectPath` (default `process.cwd()`) + - `--logFilePath` (default `log-configuration.json`) + +## 2. Multithreading & Execution Pattern + +Referencing `cmd_gridsearch.h`, the port will leverage `tf::Taskflow` and `tf::Executor` along with `moodycamel::ConcurrentQueue` for processing parallel tasks (like running a prompt against multiple items via `--each`). + +- **Architecture Details**: + 1. **Config Loading**: Read preferences/configs (using `tomlplusplus` or `rapidjson`). + 2. **Globbing / Resolution**: Resolve paths using `--include`/`--exclude`/`--each`. + 3. **Task Queueing**: For every item resolved by `--each`, queue a task. + 4. **Task Execution (Stubbed)**: The concurrent thread handles creating the LLM request. + 5. **Streaming / Output**: Results stream back (or are written to `--dst`), potentially emitting events over an IPC channel or to `stdout` depending on daemon mode setups. + +## 3. Testing Setup + +We'll replicate the testing approach found in `tests/` utilizing `Catch2` for BDD/TDD styled tests. + +- **Target Files**: + - `tests/test_cmd_kbot.cpp` + - `tests/test_kbot_run.cpp` +- **Cases to cover**: + - Validation of CLI argument defaults against `zod_schema.ts`. + - Behavior of `kbot run --list` correctly interpreting a mock `.vscode/launch.json`. + - Dry run of the `--each` pipeline ensuring tasks get initialized properly. + +## Next Steps (Scaffolding Phase) +1. Add `cmd_kbot.h/cpp` with the CLI schema variables. +2. Hook up the subcommands in `main.cpp`. +3. Stub the execution functions (`run_cmd_kbot_ai` and `run_cmd_kbot_run`) just to print out the parsed JSON representing the state. +4. Add the targets to `CMakeLists.txt` and verify the build passes. +5. Create initial Catch2 tests just to ensure the flags parse correctly without crashing.