use pyo3::prelude::*;
use pyo3::types::PyDict;
use crate::search::{tokenize as rs_tokenize, Bm25Index, bm25_search};
use crate::similarity::{lcs_similarity as rs_lcs, canonical_hash as rs_canonical};
use crate::curation::assess_text_quality as rs_quality;
use crate::pack::{validate_pack as rs_validate_pack, HivePack};
use crate::memory::Mem;
#[pymodule]
pub fn dhive_core(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(tokenize, m)?)?;
m.add_function(wrap_pyfunction!(lcs_similarity, m)?)?;
m.add_function(wrap_pyfunction!(canonical_hash, m)?)?;
m.add_function(wrap_pyfunction!(assess_text_quality, m)?)?;
m.add_function(wrap_pyfunction!(calculate_curation_score, m)?)?;
m.add_function(wrap_pyfunction!(validate_pack, m)?)?;
m.add_function(wrap_pyfunction!(bm25_search_json, m)?)?;
Ok(())
}
#[pyfunction]
fn tokenize(text: &str) -> Vec<String> {
rs_tokenize(text)
}
#[pyfunction]
fn lcs_similarity(a: &str, b: &str) -> f64 {
rs_lcs(a, b)
}
#[pyfunction]
fn canonical_hash(content: &str) -> String {
rs_canonical(content)
}
#[pyfunction]
fn assess_text_quality(content: &str) -> f64 {
rs_quality(content)
}
#[pyfunction]
fn calculate_curation_score(py: Python<'_>, params: &Bound<'_, PyDict>) -> PyResult<PyObject> {
let get_f64 = |key: &str| -> Option<f64> {
params.get_item(key).ok().flatten().and_then(|v| v.extract::<f64>().ok())
};
let get_u32 = |key: &str| -> Option<u32> {
params.get_item(key).ok().flatten().and_then(|v| v.extract::<u32>().ok())
};
let get_str = |key: &str| -> Option<String> {
params.get_item(key).ok().flatten().and_then(|v| v.extract::<String>().ok())
};
let weight = get_f64("weight").unwrap_or(0.5);
let confidence_factor = get_f64("confidenceFactor").unwrap_or(0.4);
let days_since_update = get_u32("daysSinceUpdate").unwrap_or(0);
let loaded_count = get_u32("loadedCount").unwrap_or(0);
let referenced_count = get_u32("referencedCount").unwrap_or(0);
let dispute_decay = get_f64("disputeDecay").unwrap_or(0.0);
let level = get_str("level").unwrap_or_else(|| "fact".to_string());
let content = get_str("content").unwrap_or_default();
let freshness = (1.0 - days_since_update as f64 / 180.0).max(0.0);
let text_quality = rs_quality(&content);
let usage_bonus = {
let loaded = loaded_count as f64;
let referenced = referenced_count as f64;
(loaded * 0.01 + referenced * 0.02).min(0.5)
};
let level_bonus = match level.as_str() {
"cornerstone" => 0.2,
"fact" => 0.0,
_ => -0.1,
};
let score = weight * confidence_factor * freshness * text_quality * (1.0 + usage_bonus)
- dispute_decay + level_bonus;
let dict = PyDict::new_bound(py);
dict.set_item("score", score)?;
dict.set_item("textQuality", text_quality)?;
dict.set_item("freshness", freshness)?;
dict.set_item("usageBonus", usage_bonus)?;
Ok(dict.into())
}
#[pyfunction]
fn validate_pack(py: Python<'_>, json_str: &str) -> PyResult<PyObject> {
let pack: HivePack = serde_json::from_str(json_str)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Invalid pack JSON: {}", e)))?;
let result = rs_validate_pack(&pack);
let dict = PyDict::new_bound(py);
dict.set_item("valid", result.valid)?;
dict.set_item("errors", result.errors.clone())?;
dict.set_item("warnings", result.warnings.clone())?;
dict.set_item("memoryCount", result.memory_count)?;
Ok(dict.into())
}
#[pyfunction]
fn bm25_search_json(py: Python<'_>, query: &str, memories_json: &str) -> PyResult<PyObject> {
let raw_mems: Vec<serde_json::Value> = serde_json::from_str(memories_json)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Invalid memories JSON: {}", e)))?;
let mems: Vec<Mem> = raw_mems.iter().map(|v| {
Mem {
id: v["id"].as_str().unwrap_or("?").to_string(),
level: crate::memory::MemLevel::Fact,
zone: crate::memory::MemoryZone::General,
content: v["content"].as_str().unwrap_or("").to_string(),
weight: v["weight"].as_f64().unwrap_or(0.5),
tags: vec![],
source: None,
metadata: None,
last_matched: None,
match_count: 0,
loaded_count: v["loadedCount"].as_u64().unwrap_or(0) as u32,
referenced_count: v["referencedCount"].as_u64().unwrap_or(0) as u32,
last_loaded: None,
last_referenced: None,
supersedes: None,
superseded_at: None,
superseded_by: None,
trust: None,
created_at: "".to_string(),
updated_at: "".to_string(),
}
}).collect();
let mem_refs: Vec<&Mem> = mems.iter().collect();
let index = Bm25Index::build(&mem_refs);
let results = bm25_search(query, &index, &mem_refs);
let hits: Vec<PyObject> = results.iter().map(|r| {
let mem = mems.iter().find(|m| m.id == r.id).unwrap();
let dict = PyDict::new_bound(py);
dict.set_item("id", &r.id).unwrap();
dict.set_item("score", r.score).unwrap();
dict.set_item("content", &mem.content).unwrap();
dict.into()
}).collect();
let dict = PyDict::new_bound(py);
dict.set_item("hits", hits)?;
dict.set_item("total", results.len())?;
Ok(dict.into())
}