Skip to content

Commit 645b653

Browse files
authored
Merge pull request #127 from AdaWorldAPI/claude/ada-rs-consolidation-6nvNm
feat(api): POST /api/v1/hydrate — full QualiaSnapshot endpoint
2 parents a7e3ce6 + bb68f7d commit 645b653

1 file changed

Lines changed: 351 additions & 0 deletions

File tree

src/bin/server.rs

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ use ladybug::core::simd::{self, hamming_distance};
5252
use ladybug::nars::TruthValue;
5353
use ladybug::storage::service::{CognitiveService, CpuFeatures, ServiceConfig};
5454
use ladybug::storage::{Addr, BindSpace, CogRedis, FINGERPRINT_WORDS, RedisResult};
55+
use ladybug::container::Container;
56+
use ladybug::qualia::texture::{GraphMetrics, compute as compute_texture};
57+
use ladybug::qualia::agent_state::{AgentState, PresenceMode, SelfDimensions};
58+
use ladybug::qualia::felt_parse::{GhostEcho, GhostType};
59+
use ladybug::qualia::felt_traversal::FeltPath;
60+
use ladybug::qualia::reflection::{ReflectionResult, ReflectionEntry, ReflectionOutcome};
61+
use ladybug::qualia::volition::{VolitionalAgenda, CouncilWeights};
62+
use ladybug_contract::nars::TruthValue as ContractTruthValue;
63+
use ladybug::cognitive::RungLevel;
5564
use ladybug::{FINGERPRINT_BITS, FINGERPRINT_BYTES, VERSION};
5665

5766
// =============================================================================
@@ -644,6 +653,9 @@ fn route(
644653
("POST", "/api/v1/graph/hydrate") => handle_graph_hydrate(body, state, format),
645654
("POST", "/api/v1/graph/search") => handle_graph_search(body, state, format),
646655

656+
// Qualia hydration — full QualiaSnapshot from address/fingerprint
657+
("POST", "/api/v1/hydrate") => handle_qualia_hydrate(body, state, format),
658+
647659
// LanceDB-compatible API
648660
("POST", "/api/v1/lance/table") => handle_lance_create_table(body, format),
649661
("POST", "/api/v1/lance/add") => handle_lance_add(body, state, format),
@@ -2275,6 +2287,345 @@ fn handle_graph_search(body: &str, state: &SharedState, format: ResponseFormat)
22752287
}
22762288
}
22772289

2290+
// =============================================================================
2291+
// QUALIA HYDRATION HANDLER
2292+
// =============================================================================
2293+
2294+
/// POST /api/v1/hydrate
2295+
///
2296+
/// Full qualia hydration: address/fingerprint → QualiaSnapshot (JSON).
2297+
///
2298+
/// Input (JSON):
2299+
/// ```json
2300+
/// {
2301+
/// "address": "0x8000", // hex address in BindSpace (preferred)
2302+
/// "fingerprint": "<base64>", // OR raw base64 fingerprint
2303+
/// "presence": "wife", // optional: wife|work|agi|hybrid|neutral
2304+
/// "surprise": 0.5, // optional: felt surprise [0,1]
2305+
/// "confidence": 0.6, // optional: NARS confidence [0,1]
2306+
/// "rung": 3, // optional: cognitive rung 0-9
2307+
/// "ghosts": [ // optional: active ghost echoes
2308+
/// {"type": "love", "intensity": 0.7},
2309+
/// {"type": "staunen", "intensity": 0.4}
2310+
/// ]
2311+
/// }
2312+
/// ```
2313+
///
2314+
/// Output: Full QualiaSnapshot with texture, felt physics, core axes,
2315+
/// moment awareness, mode, hints, and qualia preamble text.
2316+
fn handle_qualia_hydrate(body: &str, state: &SharedState, format: ResponseFormat) -> Vec<u8> {
2317+
// ── 1. Resolve fingerprint → Container ──────────────────────────────
2318+
let container: Container = if let Some(addr_raw) = extract_json_hex_u16(body, "address") {
2319+
let db = state.read().unwrap();
2320+
let bs = db.cog_redis.bind_space();
2321+
let addr = Addr(addr_raw);
2322+
if let Some(node) = bs.read(addr) {
2323+
Container::from(&Fingerprint::from_raw({
2324+
let mut w = [0u64; ladybug::FINGERPRINT_U64];
2325+
for (i, &word) in node.fingerprint.iter().enumerate() {
2326+
if i < w.len() {
2327+
w[i] = word;
2328+
}
2329+
}
2330+
w
2331+
}))
2332+
} else {
2333+
return http_error(404, "not_found", "Address not occupied in BindSpace", format);
2334+
}
2335+
} else if let Some(fp_b64) = extract_json_str(body, "fingerprint") {
2336+
let bytes = match base64_decode(&fp_b64) {
2337+
Ok(b) => b,
2338+
Err(_) => return http_error(400, "bad_request", "Invalid base64 fingerprint", format),
2339+
};
2340+
let mut words = [0u64; ladybug::container::CONTAINER_WORDS];
2341+
for (i, chunk) in bytes.chunks(8).enumerate() {
2342+
if i >= words.len() {
2343+
break;
2344+
}
2345+
let mut buf = [0u8; 8];
2346+
buf[..chunk.len()].copy_from_slice(chunk);
2347+
words[i] = u64::from_le_bytes(buf);
2348+
}
2349+
let mut c = Container::zero();
2350+
c.words.copy_from_slice(&words);
2351+
c
2352+
} else {
2353+
return http_error(400, "missing_field", "Need 'address' or 'fingerprint'", format);
2354+
};
2355+
2356+
// ── 2. Compute 8D texture ───────────────────────────────────────────
2357+
let metrics = GraphMetrics::default();
2358+
let texture = compute_texture(&container, &metrics);
2359+
2360+
// ── 3. Parse optional context ───────────────────────────────────────
2361+
let presence = extract_json_str(body, "presence")
2362+
.map(|s| PresenceMode::from_str(&s))
2363+
.unwrap_or_default();
2364+
2365+
let surprise = extract_json_f32(body, "surprise").unwrap_or(texture.entropy);
2366+
let confidence = extract_json_f32(body, "confidence").unwrap_or(0.5);
2367+
2368+
let rung_idx = extract_json_usize(body, "rung").unwrap_or(0) as u8;
2369+
let rung = match rung_idx {
2370+
0 => RungLevel::Surface,
2371+
1 => RungLevel::Shallow,
2372+
2 => RungLevel::Contextual,
2373+
3 => RungLevel::Analogical,
2374+
4 => RungLevel::Abstract,
2375+
5 => RungLevel::Structural,
2376+
6 => RungLevel::Counterfactual,
2377+
7 => RungLevel::Meta,
2378+
8 => RungLevel::Recursive,
2379+
9 => RungLevel::Transcendent,
2380+
_ => RungLevel::Surface,
2381+
};
2382+
2383+
// Parse ghosts from JSON array (lightweight manual parse)
2384+
let ghosts = parse_ghost_array(body);
2385+
2386+
// ── 4. Build lightweight FeltPath + Reflection for AgentState ────────
2387+
let felt_path = FeltPath {
2388+
choices: vec![],
2389+
target: ladybug::container::adjacency::PackedDn::new(&[0]),
2390+
total_surprise: surprise,
2391+
mean_surprise: surprise,
2392+
path_context: container.clone(),
2393+
};
2394+
2395+
let reflection = ReflectionResult {
2396+
entries: vec![ReflectionEntry {
2397+
dn: ladybug::container::adjacency::PackedDn::new(&[0]),
2398+
outcome: if surprise > 0.6 {
2399+
ReflectionOutcome::Explore
2400+
} else {
2401+
ReflectionOutcome::Stable
2402+
},
2403+
surprise,
2404+
truth_before: ContractTruthValue::new(0.5, confidence),
2405+
truth_after: ContractTruthValue::new(0.5, confidence),
2406+
depth: 1,
2407+
}],
2408+
felt_path: felt_path.clone(),
2409+
hydration_candidates: vec![],
2410+
};
2411+
2412+
let agenda = VolitionalAgenda {
2413+
acts: vec![],
2414+
reflection: reflection.clone(),
2415+
chains: vec![],
2416+
total_energy: surprise,
2417+
decisiveness: 0.5,
2418+
};
2419+
2420+
let council = CouncilWeights {
2421+
guardian_surprise_factor: 1.0,
2422+
catalyst_surprise_factor: 1.0,
2423+
balanced_factor: 1.0,
2424+
};
2425+
2426+
// ── 5. Compute full AgentState ──────────────────────────────────────
2427+
let agent_state = AgentState::compute(
2428+
&texture,
2429+
&felt_path,
2430+
&reflection,
2431+
&agenda,
2432+
ghosts.clone(),
2433+
rung,
2434+
council,
2435+
presence,
2436+
SelfDimensions::default(),
2437+
);
2438+
2439+
let hints = agent_state.to_hints();
2440+
let preamble = agent_state.qualia_preamble();
2441+
2442+
// ── 6. Serialize response ───────────────────────────────────────────
2443+
match format {
2444+
ResponseFormat::Arrow => {
2445+
// Arrow: return texture 8 floats + derived signals
2446+
let schema = Arc::new(Schema::new(vec![
2447+
Field::new("entropy", DataType::Float32, false),
2448+
Field::new("purity", DataType::Float32, false),
2449+
Field::new("density", DataType::Float32, false),
2450+
Field::new("bridgeness", DataType::Float32, false),
2451+
Field::new("warmth", DataType::Float32, false),
2452+
Field::new("edge", DataType::Float32, false),
2453+
Field::new("depth", DataType::Float32, false),
2454+
Field::new("flow", DataType::Float32, false),
2455+
Field::new("staunen", DataType::Float32, false),
2456+
Field::new("wisdom", DataType::Float32, false),
2457+
Field::new("ache", DataType::Float32, false),
2458+
Field::new("presence_val", DataType::Float32, false),
2459+
Field::new("tension", DataType::Float32, false),
2460+
Field::new("mode", DataType::Utf8, false),
2461+
Field::new("rung", DataType::Utf8, false),
2462+
Field::new("preamble", DataType::Utf8, false),
2463+
]));
2464+
let batch = RecordBatch::try_new(
2465+
schema,
2466+
vec![
2467+
Arc::new(Float32Array::from(vec![texture.entropy])) as ArrayRef,
2468+
Arc::new(Float32Array::from(vec![texture.purity])) as ArrayRef,
2469+
Arc::new(Float32Array::from(vec![texture.density])) as ArrayRef,
2470+
Arc::new(Float32Array::from(vec![texture.bridgeness])) as ArrayRef,
2471+
Arc::new(Float32Array::from(vec![texture.warmth])) as ArrayRef,
2472+
Arc::new(Float32Array::from(vec![texture.edge])) as ArrayRef,
2473+
Arc::new(Float32Array::from(vec![texture.depth])) as ArrayRef,
2474+
Arc::new(Float32Array::from(vec![texture.flow])) as ArrayRef,
2475+
Arc::new(Float32Array::from(vec![agent_state.felt.staunen])) as ArrayRef,
2476+
Arc::new(Float32Array::from(vec![agent_state.felt.wisdom])) as ArrayRef,
2477+
Arc::new(Float32Array::from(vec![agent_state.felt.ache])) as ArrayRef,
2478+
Arc::new(Float32Array::from(vec![agent_state.moment.presence])) as ArrayRef,
2479+
Arc::new(Float32Array::from(vec![agent_state.moment.tension])) as ArrayRef,
2480+
Arc::new(StringArray::from(vec![format!("{:?}", agent_state.mode)])) as ArrayRef,
2481+
Arc::new(StringArray::from(vec![format!("{:?}", agent_state.rung)])) as ArrayRef,
2482+
Arc::new(StringArray::from(vec![preamble.clone()])) as ArrayRef,
2483+
],
2484+
)
2485+
.unwrap();
2486+
http_arrow(200, &batch)
2487+
}
2488+
ResponseFormat::Json => {
2489+
// Build ghost JSON array
2490+
let ghost_json: Vec<String> = ghosts
2491+
.iter()
2492+
.map(|g| {
2493+
format!(
2494+
r#"{{"type":"{:?}","intensity":{:.4}}}"#,
2495+
g.ghost_type, g.intensity
2496+
)
2497+
})
2498+
.collect();
2499+
2500+
// Build hints JSON
2501+
let hints_json: Vec<String> = hints
2502+
.iter()
2503+
.map(|(k, v)| format!(r#""{}": {:.4}"#, k, v))
2504+
.collect();
2505+
2506+
let json_body = format!(
2507+
concat!(
2508+
r#"{{"texture":{{"entropy":{:.4},"purity":{:.4},"density":{:.4},"#,
2509+
r#""bridgeness":{:.4},"warmth":{:.4},"edge":{:.4},"depth":{:.4},"flow":{:.4}}},"#,
2510+
r#""felt":{{"staunen":{:.4},"wisdom":{:.4},"ache":{:.4},"libido":{:.4},"lingering":{:.4}}},"#,
2511+
r#""core":{{"alpha":{:.4},"gamma":{:.4},"omega":{:.4},"phi":{:.4}}},"#,
2512+
r#""moment":{{"now_density":{:.4},"tension":{:.4},"katharsis":{},"presence":{:.4}}},"#,
2513+
r#""presence_mode":"{:?}","mode":"{:?}","rung":"{:?}","#,
2514+
r#""ghosts":[{}],"hints":{{{}}},"preamble":{}}}"#,
2515+
),
2516+
texture.entropy,
2517+
texture.purity,
2518+
texture.density,
2519+
texture.bridgeness,
2520+
texture.warmth,
2521+
texture.edge,
2522+
texture.depth,
2523+
texture.flow,
2524+
agent_state.felt.staunen,
2525+
agent_state.felt.wisdom,
2526+
agent_state.felt.ache,
2527+
agent_state.felt.libido,
2528+
agent_state.felt.lingering,
2529+
agent_state.core.alpha,
2530+
agent_state.core.gamma,
2531+
agent_state.core.omega,
2532+
agent_state.core.phi,
2533+
agent_state.moment.now_density,
2534+
agent_state.moment.tension,
2535+
agent_state.moment.katharsis,
2536+
agent_state.moment.presence,
2537+
agent_state.presence_mode,
2538+
agent_state.mode,
2539+
agent_state.rung,
2540+
ghost_json.join(","),
2541+
hints_json.join(","),
2542+
json_escape_string(&preamble),
2543+
);
2544+
2545+
http_json(200, &json_body)
2546+
}
2547+
}
2548+
}
2549+
2550+
/// Parse ghost array from JSON body.
2551+
/// Expects: "ghosts": [{"type":"love","intensity":0.7}, ...]
2552+
fn parse_ghost_array(body: &str) -> Vec<GhostEcho> {
2553+
let mut ghosts = Vec::new();
2554+
let pattern = r#""ghosts":["#;
2555+
let start = match body.find(pattern) {
2556+
Some(s) => s + pattern.len(),
2557+
None => return ghosts,
2558+
};
2559+
let rest = &body[start..];
2560+
let end = match rest.find(']') {
2561+
Some(e) => e,
2562+
None => return ghosts,
2563+
};
2564+
let inner = &rest[..end];
2565+
2566+
// Split by "},{" to get individual ghost objects
2567+
for chunk in inner.split('}') {
2568+
let chunk = chunk.trim().trim_start_matches(',').trim();
2569+
if chunk.is_empty() {
2570+
continue;
2571+
}
2572+
// Extract type
2573+
let ghost_type = if let Some(idx) = chunk.find(r#""type":"#) {
2574+
let rest = &chunk[idx + 7..];
2575+
let rest = rest.trim().trim_start_matches('"');
2576+
let end = rest.find('"').unwrap_or(rest.len());
2577+
match rest[..end].to_lowercase().as_str() {
2578+
"love" => GhostType::Love,
2579+
"epiphany" => GhostType::Epiphany,
2580+
"arousal" => GhostType::Arousal,
2581+
"staunen" | "wonder" | "awe" => GhostType::Staunen,
2582+
"wisdom" => GhostType::Wisdom,
2583+
"thought" => GhostType::Thought,
2584+
"grief" => GhostType::Grief,
2585+
"boundary" => GhostType::Boundary,
2586+
_ => GhostType::Staunen,
2587+
}
2588+
} else {
2589+
continue;
2590+
};
2591+
2592+
// Extract intensity
2593+
let intensity = if let Some(idx) = chunk.find(r#""intensity":"#) {
2594+
let rest = &chunk[idx + 12..];
2595+
let end = rest
2596+
.find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
2597+
.unwrap_or(rest.len());
2598+
rest[..end].parse::<f32>().unwrap_or(0.5)
2599+
} else {
2600+
0.5
2601+
};
2602+
2603+
ghosts.push(GhostEcho {
2604+
ghost_type,
2605+
intensity,
2606+
});
2607+
}
2608+
ghosts
2609+
}
2610+
2611+
/// Escape a string for JSON output.
2612+
fn json_escape_string(s: &str) -> String {
2613+
let mut out = String::with_capacity(s.len() + 2);
2614+
out.push('"');
2615+
for c in s.chars() {
2616+
match c {
2617+
'"' => out.push_str("\\\""),
2618+
'\\' => out.push_str("\\\\"),
2619+
'\n' => out.push_str("\\n"),
2620+
'\r' => out.push_str("\\r"),
2621+
'\t' => out.push_str("\\t"),
2622+
c => out.push(c),
2623+
}
2624+
}
2625+
out.push('"');
2626+
out
2627+
}
2628+
22782629
// =============================================================================
22792630
// UDP BITPACKED HAMMING HANDLER
22802631
// =============================================================================

0 commit comments

Comments
 (0)