// ============================================================================ // SyFox core C API — thin shared-library wrapper (server bridge). // All decisions come from the SI substrate; this file only marshals JSON // and exposes the SAME boundary knobs the CLI already has (v1.0 -> v3.0): // // M1 energy-norm measurement gain -> syfox_engine_set_energy_norm // v1 SI salience gating + Miller window -> syfox_engine_set_source_modes // M5 deterministic OMP parallel settle -> syfox_engine_set_parallel_settle // v2.2 trigram lane policy -> syfox_engine_set_ngrams // v2.2 script routing -> syfox_detect_script // M3 machine-auditable evidence -> syfox_decide_ex(opts.evidence) // v3.2 semantic layer + retrieval + hierarchy -> set_semantics / set_retrieval / // set_retrieval_topk / set_retrieval_dose / set_hierarchy + opts keys // // No transformer, no classifier, no physics changes: si_substrate.hpp is // untouched; every knob here exists because the CLI exposes it too. // ============================================================================ #include "core/syfox.hpp" #include "core/script.hpp" #include "core/ngram.hpp" #include #include using syfox::Engine; namespace { bool truthy(const sfx::JV& v) { return (v.is_bool() && v.b) || (v.is_num() && v.num != 0.0); } } extern "C" { struct SyFoxHandle { Engine* eng; }; SyFoxHandle* syfox_engine_create(const char* model_dir) { if (!model_dir) return nullptr; SyFoxHandle* h = new SyFoxHandle(); h->eng = new Engine(); h->eng->load_model(model_dir); return h; } void syfox_engine_free(SyFoxHandle* h) { if (!h) return; delete h->eng; delete h; } const char* syfox_core_version(void) { return syfox::VERSION; } // -- engine-scoped knobs (identical semantics to the CLI flags) ------------- void syfox_engine_set_energy_norm(SyFoxHandle* h, int on) { if (h) h->eng->set_energy_norm(on != 0); // M1: measurement gain } void syfox_engine_set_source_modes(SyFoxHandle* h, int salience_gating, int miller_window) { if (h) h->eng->substrate().set_source_modes(salience_gating != 0, miller_window != 0); } void syfox_engine_set_parallel_settle(SyFoxHandle* h, int on, int threads) { if (!h) return; #if defined(_OPENMP) if (threads > 0) omp_set_num_threads(threads); #endif h->eng->substrate().set_parallel_settle(on != 0); // M5: OMP builds only } void syfox_engine_set_ngrams(SyFoxHandle* h, int on) { (void)h; // process-global policy si::norm::grams_enabled() = (on != 0); // v2.2 trigram lanes } // -- v3.2 semantic layer / retrieval-by-default / hierarchy knobs ------------- void syfox_engine_set_semantics(SyFoxHandle* h, int on) { if (h) h->eng->substrate().set_semantics(on != 0); } void syfox_engine_set_retrieval(SyFoxHandle* h, int on) { if (h) h->eng->set_retrieval(on != 0); } void syfox_engine_set_retrieval_topk(SyFoxHandle* h, int topk) { if (h) h->eng->set_retrieval_topk(topk); } void syfox_engine_set_retrieval_dose(SyFoxHandle* h, double dose) { if (h) h->eng->set_retrieval_dose(static_cast(dose)); } void syfox_engine_set_hierarchy(SyFoxHandle* h, int on) { if (h) h->eng->set_hierarchy(on != 0); } // -- script detection (v2.2 --lang auto) ------------------------------------ // Returns a malloc'd slug ("latin", "bengali", ...) or "unknown". char* syfox_detect_script(const char* text) { if (!text) return nullptr; const si::script::Script sc = si::script::detect_script(text); const std::string slug = (sc == si::script::Script::Unknown) ? "unknown" : si::script::slug(sc); char* buf = static_cast(std::malloc(slug.size() + 1)); std::memcpy(buf, slug.c_str(), slug.size() + 1); return buf; } // -- decide ------------------------------------------------------------------ // Shared marshaling for syfox_decide (no evidence) and syfox_decide_ex. static std::string decide_json(SyFoxHandle* h, const char* state, const char* questions_json, bool want_evidence) { std::string out; try { sfx::JV q = sfx::JV::parse(questions_json); if (!q.is_obj()) { return "{\"error\":\"questions must be an object\"}"; } syfox::Usage u; auto answers = h->eng->decide(state, q, u); u.calibrated = h->eng->calibration().fitted; // decide() resets Usage sfx::JVObj ans; for (const auto& a : answers) { sfx::JVObj o; o["type"] = sfx::JV(a.type); if (a.type == "choice") { o["choice"] = sfx::JV(a.choice); sfx::JVObj pr; for (const auto& p : a.probabilities) pr[p.first] = sfx::JV(std::round(p.second * 1000.0f) / 1000.0f); o["probabilities"] = sfx::JV(pr); } else if (a.type == "score") { o["score"] = sfx::JV(std::round(a.value * 1000.0f) / 1000.0f); sfx::JVObj pr; for (const auto& p : a.probabilities) pr[p.first] = sfx::JV(std::round(p.second * 1000.0f) / 1000.0f); o["probabilities"] = sfx::JV(pr); } else if (a.type == "noul") { o["noul"] = sfx::JV(std::round(a.probability * 1000.0f) / 1000.0f); } o["confidence"] = sfx::JV(std::round(a.confidence * 1000.0f) / 1000.0f); o["deferred"] = sfx::JV(a.deferred); if (!a.reason.empty()) o["reason"] = sfx::JV(a.reason); ans[a.qid] = sfx::JV(o); } sfx::JVObj usage{ {"state_tokens", static_cast(u.state_tokens)}, {"vocabulary", static_cast(u.vocabulary)}, {"lanes", static_cast(u.lanes)}, {"settled_energy", std::round(u.settled_energy * 1000.0f) / 1000.0f}, {"calibrated", sfx::JV(u.calibrated)}, {"engine", std::string("syfox-") + syfox::VERSION}, {"core", "si-substrate"}}; if (!u.retrieved.empty()) { sfx::JVArr ret; for (const auto& r : u.retrieved) ret.push_back(sfx::JV(sfx::JVObj{ {"label", sfx::JV(r.first)}, {"resonance", std::round(r.second * 1000.0f) / 1000.0f}})); usage["retrieval"] = sfx::JV(ret); // v3.2: priming memories } sfx::JVObj root{{"answers", sfx::JV(ans)}, {"usage", sfx::JV(usage)}}; if (want_evidence) // M3: supporting lanes + provenance + contradictions root["evidence"] = h->eng->evidence_json(state, q, answers); out = sfx::JV(root).dump(); } catch (const std::exception& e) { out = std::string("{\"error\":\"") + e.what() + "\"}"; } return out; } // Decides and returns a malloc'd JSON string: {"answers":..., "usage":...}. // Caller frees with syfox_string_free. On malformed questions returns // {"error": "..."}. char* syfox_decide(SyFoxHandle* h, const char* state, const char* questions_json) { if (!h || !state || !questions_json) return nullptr; const std::string s = decide_json(h, state, questions_json, false); char* buf = static_cast(std::malloc(s.size() + 1)); std::memcpy(buf, s.c_str(), s.size() + 1); return buf; } // opts_json: {"evidence":bool, "energy_norm":bool, "salience_gating":bool, // "miller_window":bool, "ngrams":"on"|"off", // "semantics":bool, "retrieval":bool, "retrieval_topk":int, // "retrieval_dose":num, "hierarchy":bool} // Absent keys leave the current engine state untouched (sticky, CLI-equal). char* syfox_decide_ex(SyFoxHandle* h, const char* state, const char* questions_json, const char* opts_json) { if (!h || !state || !questions_json) return nullptr; bool evidence = false; if (opts_json && *opts_json) { try { sfx::JV o = sfx::JV::parse(opts_json); if (o.is_obj()) { if (o.has("evidence")) evidence = truthy(o.at("evidence")); if (o.has("energy_norm")) h->eng->set_energy_norm(truthy(o.at("energy_norm"))); if (o.has("salience_gating") || o.has("miller_window")) h->eng->substrate().set_source_modes( o.has("salience_gating") && truthy(o.at("salience_gating")), o.has("miller_window") && truthy(o.at("miller_window"))); if (o.has("ngrams")) { const std::string g = o.at("ngrams").as_str(); if (g == "on") si::norm::grams_enabled() = true; if (g == "off") si::norm::grams_enabled() = false; } if (o.has("semantics")) h->eng->substrate().set_semantics(truthy(o.at("semantics"))); if (o.has("retrieval")) h->eng->set_retrieval(truthy(o.at("retrieval"))); if (o.has("retrieval_topk")) h->eng->set_retrieval_topk(static_cast(o.at("retrieval_topk").as_num(5))); if (o.has("retrieval_dose")) h->eng->set_retrieval_dose(static_cast(o.at("retrieval_dose").as_num(0.30))); if (o.has("hierarchy")) h->eng->set_hierarchy(truthy(o.at("hierarchy"))); } } catch (...) { /* opts are optional; a bad opts object is ignored */ } } const std::string s = decide_json(h, state, questions_json, evidence); char* buf = static_cast(std::malloc(s.size() + 1)); std::memcpy(buf, s.c_str(), s.size() + 1); return buf; } // Model fabric summary: {"nodes":...,"lanes":...,"evidence_records":..., // "calibrated":...,"version":...,"core":...} char* syfox_engine_info(SyFoxHandle* h) { if (!h) return nullptr; const sfx::JVObj o{ {"nodes", static_cast(h->eng->substrate().node_count())}, {"lanes", static_cast(h->eng->substrate().lane_count())}, {"evidence_records", static_cast(h->eng->substrate().evidence_count())}, {"calibrated", sfx::JV(h->eng->calibration().fitted)}, {"semantics", sfx::JV(h->eng->substrate().has_semantics())}, {"sem_edges", static_cast(h->eng->substrate().resonance_edge_count())}, {"lane_contexts", static_cast(h->eng->substrate().lane_context_count())}, {"retrieval_memories", static_cast(h->eng->memories().size())}, {"hierarchy", sfx::JV(h->eng->hierarchy_on())}, {"version", std::string(syfox::VERSION)}, {"core", "si-substrate"}}; const std::string s = sfx::JV(o).dump(); char* buf = static_cast(std::malloc(s.size() + 1)); std::memcpy(buf, s.c_str(), s.size() + 1); return buf; } void syfox_string_free(char* s) { std::free(s); } } // extern "C"