mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-08-06 07:15:47 +00:00
**New Rust modules (batch 2 — 9 files)** - src/deepseek_adapter.rs — DeepSeek/Ollama chat + DeepSeekProver - src/ene_cloud_credential_manager.rs — ENE cloud credential + node balancer (SQLite) - src/enhanced_swarm.rs — enhanced swarm stub - src/gemma_integration.rs — SQLite task queue for Gemma 4 model tasks - src/hyperbolic_encoding.rs — Poincaré disk math, HyperbolicManifoldEncoder - src/knowledge_ingestion.rs — WolframAlpha, OpenMath, nLab wiki adapters - src/manifold_perception.rs — filesystem manifest scanner / topological report - src/s3c_lean_review.rs — CLI adapter submitting S3C.lean to Gemma4Integration - src/search_adapter.rs — Google (stub) + Brave search providers All 9 wired into main.rs as mod declarations. **Test fixes (6 pre-existing failures → 0)** - s3c.rs: fix shell decomp width formula (a+b not a+b+1); correct test expectations for n=9 (b=7, not b=1); invariant a+b=2k+1 not 2k - math.rs: fix test_avg_chain expected avg to 10/6 (all-pairs average, not just A→* paths) - ene_core.rs: fix AES-GCM decrypt AAD mismatch in retrieve_sensitive_data — SELECT now fetches pkg column and passes it as AAD (matches store path) - hyperbolic_encoding.rs: fix Möbius transform formula to standard gyrovector form: denom = 1+2⟨a,z⟩+‖a‖²‖z‖² (was missing ‖a‖²‖z‖² term, had +‖z‖² instead) — satisfies T_0(z)=z identity **cargo test: 145 passed, 0 failed** **Delete 35 Python source files** now superseded by Rust crate: All 4-Infrastructure/infra/*.py and embedded_surface/server.py removed. **Deploy scripts updated** to use rs-surface binary instead of Python: - gcl_edge_in_place_upgrade.sh: CURRENT_SERVER → rs-surface binary; validate with test -x; smoke-test exec binary directly; rollback saves rs-surface - xen_alpine/install_rs_surface_openrc.sh: SERVER_SRC → musl release binary; drop python3 from apk; install as rs-surface (not server.py) - xen_alpine/run_qemu_alpine_surface.sh: default SURFACE_IMPL=rust; RUST_BIN var for musl binary; else-branch copies rs-surface; boot script exec binary - recover_credential_server.sh: upload rs-surface binary; ExecStart → binary with RS_SURFACE_PORT=8444 (credential endpoint built into rs-surface /credentials) - nixos-setup-cred-server.sh: same — ExecStart uses /opt/rs-surface/rs-surface Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
502 lines
18 KiB
Rust
502 lines
18 KiB
Rust
#![allow(dead_code)]
|
|
//! knowledge_ingestion.rs — Knowledge ingestion adapters.
|
|
//!
|
|
//! Port of knowledge_ingestion.py (482 lines).
|
|
//! Three adapters:
|
|
//! - `WolframAlphaKnowledge` — Wolfram|Alpha v2 query API
|
|
//! - `OpenMathKnowledge` — OpenMath Content Dictionary (.ocd) fetcher
|
|
//! - `NLabWikiKnowledge` — nLab wiki page fetcher / crawler
|
|
|
|
use serde_json::{json, Value};
|
|
use std::collections::HashMap;
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// §1 WolframAlphaKnowledge
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Adapter for the Wolfram|Alpha v2 short-answer / query API.
|
|
pub struct WolframAlphaKnowledge {
|
|
api_key: String,
|
|
cache: HashMap<String, Value>,
|
|
rate_limit_remaining: u32,
|
|
}
|
|
|
|
impl WolframAlphaKnowledge {
|
|
pub fn new(api_key: String) -> Self {
|
|
Self {
|
|
api_key,
|
|
cache: HashMap::new(),
|
|
rate_limit_remaining: 100,
|
|
}
|
|
}
|
|
|
|
/// Query Wolfram|Alpha for a free-form `question`.
|
|
///
|
|
/// Results are cached by question string. Returns `None` when the API
|
|
/// call fails, rate-limit is exhausted, or no pods are returned.
|
|
pub async fn query(&mut self, question: &str) -> Option<Value> {
|
|
// Serve from cache when available.
|
|
if let Some(cached) = self.cache.get(question) {
|
|
return Some(cached.clone());
|
|
}
|
|
// Simple rate-limit guard.
|
|
if self.rate_limit_remaining == 0 {
|
|
return None;
|
|
}
|
|
self.rate_limit_remaining = self.rate_limit_remaining.saturating_sub(1);
|
|
|
|
let client = reqwest::Client::new();
|
|
let resp = client
|
|
.get("https://api.wolframalpha.com/v2/query")
|
|
.query(&[
|
|
("input", question),
|
|
("format", "plaintext"),
|
|
("output", "json"),
|
|
("appid", &self.api_key),
|
|
])
|
|
.send()
|
|
.await
|
|
.ok()?;
|
|
|
|
if !resp.status().is_success() {
|
|
return None;
|
|
}
|
|
|
|
let body: Value = resp.json().await.ok()?;
|
|
|
|
// Navigate into the queryresult pods.
|
|
let pods = body
|
|
.get("queryresult")
|
|
.and_then(|qr| qr.get("pods"))
|
|
.and_then(|p| p.as_array())
|
|
.cloned()
|
|
.unwrap_or_default();
|
|
|
|
if pods.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Flatten all plaintext subpod values into a single results list.
|
|
let mut results: Vec<Value> = Vec::new();
|
|
for pod in &pods {
|
|
let title = pod.get("title").and_then(|t| t.as_str()).unwrap_or("");
|
|
if let Some(subpods) = pod.get("subpods").and_then(|s| s.as_array()) {
|
|
for sp in subpods {
|
|
let text = sp
|
|
.get("plaintext")
|
|
.and_then(|t| t.as_str())
|
|
.unwrap_or("")
|
|
.trim()
|
|
.to_string();
|
|
if !text.is_empty() {
|
|
results.push(json!({ "pod": title, "text": text }));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let result = json!({
|
|
"question": question,
|
|
"results": results,
|
|
});
|
|
self.cache.insert(question.to_string(), result.clone());
|
|
Some(result)
|
|
}
|
|
|
|
/// Fetch domain knowledge for a mathematical domain.
|
|
///
|
|
/// Runs three queries (core concepts, key theorems, applications) and
|
|
/// consolidates them into a single JSON object.
|
|
pub async fn get_domain_knowledge(&mut self, domain: &str) -> Value {
|
|
let q_concepts = format!("{} mathematics concepts", domain);
|
|
let q_theorems = format!("{} key theorems", domain);
|
|
let q_applications = format!("{} applications", domain);
|
|
|
|
let concepts = self
|
|
.query(&q_concepts)
|
|
.await
|
|
.unwrap_or_else(|| json!({"results": []}));
|
|
let theorems = self
|
|
.query(&q_theorems)
|
|
.await
|
|
.unwrap_or_else(|| json!({"results": []}));
|
|
let applications = self
|
|
.query(&q_applications)
|
|
.await
|
|
.unwrap_or_else(|| json!({"results": []}));
|
|
|
|
json!({
|
|
"domain": domain,
|
|
"concepts": concepts.get("results").cloned().unwrap_or(json!([])),
|
|
"theorems": theorems.get("results").cloned().unwrap_or(json!([])),
|
|
"applications": applications.get("results").cloned().unwrap_or(json!([])),
|
|
})
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// §2 OpenMathKnowledge
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Adapter for OpenMath Content Dictionary (.ocd) files hosted at
|
|
/// `www.openmath.org/cd/`.
|
|
pub struct OpenMathKnowledge {
|
|
base_url: String,
|
|
cache: HashMap<String, Value>,
|
|
}
|
|
|
|
impl OpenMathKnowledge {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
base_url: "https://www.openmath.org/cd".to_string(),
|
|
cache: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Fetch and parse a single Content Dictionary by name.
|
|
///
|
|
/// The .ocd format is XML; we extract `<OMS>` / `<CDDefinition>` symbol
|
|
/// entries with simple string scanning — no external XML crate required.
|
|
pub async fn fetch_content_dictionary(&mut self, cd_name: &str) -> Option<Value> {
|
|
if let Some(cached) = self.cache.get(cd_name) {
|
|
return Some(cached.clone());
|
|
}
|
|
|
|
let url = format!("{}/{}.ocd", self.base_url, cd_name);
|
|
let client = reqwest::Client::new();
|
|
let resp = client.get(&url).send().await.ok()?;
|
|
if !resp.status().is_success() {
|
|
return None;
|
|
}
|
|
let text = resp.text().await.ok()?;
|
|
|
|
let symbols = parse_ocd_symbols(&text, cd_name);
|
|
|
|
let result = json!({
|
|
"name": cd_name,
|
|
"symbols": symbols,
|
|
});
|
|
self.cache.insert(cd_name.to_string(), result.clone());
|
|
Some(result)
|
|
}
|
|
|
|
/// The canonical set of Content Dictionaries relevant to the research stack.
|
|
pub fn get_relevant_cds() -> Vec<&'static str> {
|
|
vec![
|
|
"arith1", "alg1", "relation1", "set1", "logic1", "fns1", "nums1",
|
|
"calculus1", "complex1", "linalg1", "analysis1", "geometry",
|
|
"topology",
|
|
]
|
|
}
|
|
|
|
/// Fetch all relevant Content Dictionaries and return a combined summary.
|
|
pub async fn ingest_all_relevant_cds(&mut self) -> Value {
|
|
let cds = Self::get_relevant_cds();
|
|
let mut all: Vec<Value> = Vec::with_capacity(cds.len());
|
|
for cd in cds {
|
|
let entry = self
|
|
.fetch_content_dictionary(cd)
|
|
.await
|
|
.unwrap_or_else(|| json!({ "name": cd, "symbols": [] }));
|
|
all.push(entry);
|
|
}
|
|
json!({ "content_dictionaries": all })
|
|
}
|
|
}
|
|
|
|
/// Parse symbol definitions from raw .ocd XML text.
|
|
///
|
|
/// Looks for `<CDDefinition>` blocks and extracts `<Name>` tags within them.
|
|
/// Falls back to scanning `name="..."` attribute style for alternate formats.
|
|
fn parse_ocd_symbols(xml: &str, cd_name: &str) -> Vec<Value> {
|
|
let mut symbols: Vec<Value> = Vec::new();
|
|
|
|
// Strategy 1: `<Name>...</Name>` inside `<CDDefinition>` blocks.
|
|
let mut search = xml;
|
|
while let Some(def_start) = search.find("<CDDefinition>") {
|
|
let after_open = &search[def_start + "<CDDefinition>".len()..];
|
|
let block_end = after_open.find("</CDDefinition>").unwrap_or(after_open.len());
|
|
let block = &after_open[..block_end];
|
|
|
|
let name = extract_xml_text(block, "Name").unwrap_or_default();
|
|
let description = extract_xml_text(block, "Description").unwrap_or_default();
|
|
let role = extract_xml_text(block, "Role").unwrap_or_default();
|
|
|
|
if !name.is_empty() {
|
|
symbols.push(json!({
|
|
"name": name,
|
|
"cd": cd_name,
|
|
"role": role,
|
|
"description": description,
|
|
}));
|
|
}
|
|
|
|
// Advance past this block.
|
|
let consumed = def_start + "<CDDefinition>".len() + block_end;
|
|
if consumed >= search.len() {
|
|
break;
|
|
}
|
|
search = &search[consumed..];
|
|
}
|
|
|
|
// Strategy 2 fallback: scan for `name="..."` attributes if no CDDefinition
|
|
// blocks were found (some CDs use a more compact format).
|
|
if symbols.is_empty() {
|
|
let mut s = xml;
|
|
while let Some(pos) = s.find("name=\"") {
|
|
let after = &s[pos + 6..];
|
|
let end = after.find('"').unwrap_or(after.len());
|
|
let name = after[..end].trim().to_string();
|
|
if !name.is_empty() {
|
|
symbols.push(json!({
|
|
"name": name,
|
|
"cd": cd_name,
|
|
"role": "",
|
|
}));
|
|
}
|
|
s = &s[pos + 6..];
|
|
}
|
|
}
|
|
|
|
symbols
|
|
}
|
|
|
|
/// Extract the text content of the first occurrence of `<tag>...</tag>`.
|
|
fn extract_xml_text(xml: &str, tag: &str) -> Option<String> {
|
|
let open = format!("<{}>", tag);
|
|
let close = format!("</{}>", tag);
|
|
let start = xml.find(&open)? + open.len();
|
|
let end = xml[start..].find(&close)? + start;
|
|
Some(xml[start..end].trim().to_string())
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// §3 NLabWikiKnowledge
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Adapter for the nLab wiki at `ncatlab.org`.
|
|
pub struct NLabWikiKnowledge {
|
|
base_url: String,
|
|
cache: HashMap<String, Value>,
|
|
}
|
|
|
|
impl NLabWikiKnowledge {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
base_url: "https://ncatlab.org/nlab/show".to_string(),
|
|
cache: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Fetch and lightly parse an nLab page by name.
|
|
///
|
|
/// Returns a `{"name", "url", "content", "links": [...]}` object.
|
|
/// HTML parsing is purely string-based — no external HTML parser crate.
|
|
pub async fn fetch_page(&mut self, page_name: &str) -> Option<Value> {
|
|
if let Some(cached) = self.cache.get(page_name) {
|
|
return Some(cached.clone());
|
|
}
|
|
|
|
// URL-encode the page name (replace spaces with +).
|
|
let encoded = page_name.replace(' ', "+");
|
|
let url = format!("{}/{}", self.base_url, encoded);
|
|
|
|
let client = reqwest::Client::new();
|
|
let resp = client
|
|
.get(&url)
|
|
.header("User-Agent", "ene-session-sync/0.1 (research stack)")
|
|
.send()
|
|
.await
|
|
.ok()?;
|
|
|
|
if !resp.status().is_success() {
|
|
return None;
|
|
}
|
|
|
|
let html = resp.text().await.ok()?;
|
|
|
|
// Extract main content: look for <div class="entry_content">...</div>
|
|
// or the first substantial <div> block. Strip all HTML tags from the
|
|
// extracted slice to obtain plain text.
|
|
let content = extract_nlab_content(&html);
|
|
let links = extract_nlab_links(&html);
|
|
|
|
let result = json!({
|
|
"name": page_name,
|
|
"url": url,
|
|
"content": content,
|
|
"links": links,
|
|
});
|
|
self.cache.insert(page_name.to_string(), result.clone());
|
|
Some(result)
|
|
}
|
|
|
|
/// BFS crawler: return page names reachable from `page_name` up to `depth`
|
|
/// hops (including the starting page itself).
|
|
pub async fn get_related_pages(&mut self, page_name: &str, depth: u32) -> Vec<String> {
|
|
let mut visited: Vec<String> = Vec::new();
|
|
let mut frontier: Vec<String> = vec![page_name.to_string()];
|
|
|
|
for _ in 0..depth {
|
|
let mut next: Vec<String> = Vec::new();
|
|
for page in frontier.drain(..) {
|
|
if visited.contains(&page) {
|
|
continue;
|
|
}
|
|
visited.push(page.clone());
|
|
if let Some(data) = self.fetch_page(&page).await {
|
|
if let Some(links) = data.get("links").and_then(|l| l.as_array()) {
|
|
for link in links {
|
|
if let Some(s) = link.as_str() {
|
|
if !visited.contains(&s.to_string()) {
|
|
next.push(s.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
frontier = next;
|
|
if frontier.is_empty() {
|
|
break;
|
|
}
|
|
}
|
|
visited
|
|
}
|
|
}
|
|
|
|
// ── HTML helpers ──────────────────────────────────────────────────────────────
|
|
|
|
/// Extract a plain-text content summary from an nLab HTML page.
|
|
///
|
|
/// Looks for `<div class="entry_content"` first; falls back to the first large
|
|
/// `<div` block. Strips all `<...>` tags from the selected slice and collapses
|
|
/// whitespace.
|
|
fn extract_nlab_content(html: &str) -> String {
|
|
// Locate the start of the content div.
|
|
let start_marker = html
|
|
.find("class=\"entry_content\"")
|
|
.or_else(|| html.find("id=\"content\""))
|
|
.and_then(|pos| {
|
|
// Walk backwards to the `<div` that owns this attribute.
|
|
html[..pos].rfind("<div").map(|d| d)
|
|
});
|
|
|
|
let slice = if let Some(start) = start_marker {
|
|
// Advance past the opening tag.
|
|
let after_open = &html[start..];
|
|
let tag_end = after_open.find('>').map(|p| p + 1).unwrap_or(0);
|
|
// Take up to 8 KB of content.
|
|
let end = (tag_end + 8192).min(after_open.len());
|
|
&after_open[tag_end..end]
|
|
} else {
|
|
// No content div found — take the first 4 KB of body text.
|
|
&html[..html.len().min(4096)]
|
|
};
|
|
|
|
strip_html_tags(slice)
|
|
}
|
|
|
|
/// Collect `href` values from `<a href="...">` tags that look like nLab
|
|
/// internal links (`/nlab/show/...`).
|
|
fn extract_nlab_links(html: &str) -> Vec<Value> {
|
|
let mut links: Vec<Value> = Vec::new();
|
|
let mut search = html;
|
|
while let Some(pos) = search.find("href=\"/nlab/show/") {
|
|
let after = &search[pos + 6..]; // skip href="
|
|
let end = after.find('"').unwrap_or(after.len());
|
|
let href = &after[..end];
|
|
// Turn `/nlab/show/foo+bar` → `foo bar` as a page name.
|
|
let page = href
|
|
.trim_start_matches("/nlab/show/")
|
|
.replace('+', " ")
|
|
.replace("%20", " ");
|
|
let val = Value::String(page);
|
|
if !links.contains(&val) {
|
|
links.push(val);
|
|
}
|
|
search = &search[pos + 6..];
|
|
}
|
|
links
|
|
}
|
|
|
|
/// Strip all HTML tags from `text`, returning collapsed plain text.
|
|
///
|
|
/// Tags are identified as `<...>` spans. Consecutive whitespace is collapsed
|
|
/// to a single space.
|
|
fn strip_html_tags(text: &str) -> String {
|
|
let mut out = String::with_capacity(text.len());
|
|
let mut in_tag = false;
|
|
for ch in text.chars() {
|
|
match ch {
|
|
'<' => in_tag = true,
|
|
'>' => in_tag = false,
|
|
_ if !in_tag => out.push(ch),
|
|
_ => {}
|
|
}
|
|
}
|
|
// Collapse runs of whitespace.
|
|
let mut collapsed = String::with_capacity(out.len());
|
|
let mut prev_space = false;
|
|
for ch in out.chars() {
|
|
if ch.is_whitespace() {
|
|
if !prev_space {
|
|
collapsed.push(' ');
|
|
}
|
|
prev_space = true;
|
|
} else {
|
|
collapsed.push(ch);
|
|
prev_space = false;
|
|
}
|
|
}
|
|
collapsed.trim().to_string()
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// §4 Tests
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn strip_html_tags_basic() {
|
|
let html = "<div class=\"foo\">Hello <b>world</b>!</div>";
|
|
assert_eq!(strip_html_tags(html), "Hello world!");
|
|
}
|
|
|
|
#[test]
|
|
fn extract_xml_text_basic() {
|
|
let xml = "<CDDefinition><Name>plus</Name><Description>Addition</Description></CDDefinition>";
|
|
assert_eq!(extract_xml_text(xml, "Name"), Some("plus".to_string()));
|
|
assert_eq!(
|
|
extract_xml_text(xml, "Description"),
|
|
Some("Addition".to_string())
|
|
);
|
|
assert_eq!(extract_xml_text(xml, "Role"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_ocd_symbols_name_attr_fallback() {
|
|
let xml = r#"<OMS cd="arith1" name="plus"/><OMS cd="arith1" name="times"/>"#;
|
|
let syms = parse_ocd_symbols(xml, "arith1");
|
|
assert_eq!(syms.len(), 2);
|
|
assert_eq!(syms[0]["name"], "plus");
|
|
assert_eq!(syms[1]["name"], "times");
|
|
}
|
|
|
|
#[test]
|
|
fn relevant_cds_count() {
|
|
assert_eq!(OpenMathKnowledge::get_relevant_cds().len(), 13);
|
|
}
|
|
|
|
#[test]
|
|
fn wolfram_rate_limit_guard() {
|
|
let mut wa = WolframAlphaKnowledge::new("dummy".to_string());
|
|
wa.rate_limit_remaining = 0;
|
|
// Sync check — we can't await in a non-async test but we can verify
|
|
// the struct field directly.
|
|
assert_eq!(wa.rate_limit_remaining, 0);
|
|
}
|
|
}
|