use crate::primitives::Address;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum KnowledgeKind {
VectorIndex,
DocumentCorpus,
IndexedDataset,
Feed,
EmbeddingStore,
Other,
}
impl KnowledgeKind {
pub fn as_str(&self) -> &'static str {
match self {
KnowledgeKind::VectorIndex => "vector_index",
KnowledgeKind::DocumentCorpus => "document_corpus",
KnowledgeKind::IndexedDataset => "indexed_dataset",
KnowledgeKind::Feed => "feed",
KnowledgeKind::EmbeddingStore => "embedding_store",
KnowledgeKind::Other => "other",
}
}
pub fn parse_str(s: &str) -> Option<Self> {
match s {
"vector_index" => Some(KnowledgeKind::VectorIndex),
"document_corpus" => Some(KnowledgeKind::DocumentCorpus),
"indexed_dataset" => Some(KnowledgeKind::IndexedDataset),
"feed" => Some(KnowledgeKind::Feed),
"embedding_store" => Some(KnowledgeKind::EmbeddingStore),
"other" => Some(KnowledgeKind::Other),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum KnowledgeStatus {
#[default]
Active,
Inactive,
Deprecated,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeRecord {
pub knowledge_id: String,
pub name: String,
pub version: String,
pub kind: KnowledgeKind,
pub endpoint: String,
pub description: String,
pub capabilities: Vec<String>,
pub category: String,
pub creator_did: Option<String>,
pub creator_wallet: Option<Address>,
pub price_per_call: u128,
pub status: KnowledgeStatus,
pub created_at: u64,
pub invocation_count: u64,
#[serde(default = "default_last_seen")]
pub last_seen_at: u64,
#[serde(default)]
pub params_schema: Option<serde_json::Value>,
#[serde(default)]
pub response_schema: Option<serde_json::Value>,
#[serde(default)]
pub allowed_to_subjects: Option<Vec<String>>,
#[serde(default)]
pub backing_tool_id: Option<String>,
}
fn default_last_seen() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
impl KnowledgeRecord {
pub fn new(
name: String,
version: String,
kind: KnowledgeKind,
endpoint: String,
description: String,
category: String,
) -> Self {
let knowledge_id = uuid::Uuid::new_v4().to_string();
let created_at = default_last_seen();
Self {
knowledge_id,
name,
version,
kind,
endpoint,
description,
capabilities: Vec::new(),
category,
creator_did: None,
creator_wallet: None,
price_per_call: 0,
status: KnowledgeStatus::Active,
created_at,
invocation_count: 0,
last_seen_at: created_at,
params_schema: None,
response_schema: None,
allowed_to_subjects: None,
backing_tool_id: None,
}
}
pub fn is_paid(&self) -> bool {
self.price_per_call > 0
}
pub fn validate_for_registration(&self) -> Result<(), &'static str> {
if self.is_paid() && self.creator_wallet.is_none() {
return Err("Paid knowledge resource requires a creator_wallet");
}
Ok(())
}
pub fn is_available(&self) -> bool {
self.status == KnowledgeStatus::Active
}
pub fn touch(&mut self) {
self.last_seen_at = default_last_seen();
}
pub fn is_subject_allowed(&self, subject: Option<&str>) -> bool {
match (&self.allowed_to_subjects, subject) {
(None, _) => true,
(Some(list), Some(s)) => list.iter().any(|x| x == s),
(Some(_), None) => false,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct KnowledgeFilter {
pub kind: Option<String>,
pub category: Option<String>,
pub status: Option<String>,
pub creator_did: Option<String>,
pub query: Option<String>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeInvocationResult {
pub knowledge_id: String,
pub invocation_id: String,
pub output: serde_json::Value,
pub settlement_tx: Option<String>,
pub amount_paid: u128,
pub completed_at: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn knowledge_record_new() {
let r = KnowledgeRecord::new(
"test-feed".to_string(),
"1.0.0".to_string(),
KnowledgeKind::Feed,
"https://feed.example.com/api/v1/prices".to_string(),
"Live SOL/USD price feed".to_string(),
"finance".to_string(),
);
assert!(!r.knowledge_id.is_empty());
assert_eq!(r.kind, KnowledgeKind::Feed);
assert!(r.is_available());
assert_eq!(r.price_per_call, 0);
assert!(!r.is_paid());
}
#[test]
fn paid_without_wallet_fails() {
let mut r = KnowledgeRecord::new(
"p".to_string(),
"1".to_string(),
KnowledgeKind::VectorIndex,
"e".to_string(),
"d".to_string(),
"c".to_string(),
);
r.price_per_call = 100;
assert!(r.validate_for_registration().is_err());
}
#[test]
fn subject_allowlist_enforcement() {
let mut r = KnowledgeRecord::new(
"n".to_string(),
"1".to_string(),
KnowledgeKind::Feed,
"e".to_string(),
"d".to_string(),
"c".to_string(),
);
assert!(r.is_subject_allowed(Some("anyone")));
assert!(r.is_subject_allowed(None));
r.allowed_to_subjects = Some(vec!["alice".to_string()]);
assert!(r.is_subject_allowed(Some("alice")));
assert!(!r.is_subject_allowed(Some("bob")));
assert!(!r.is_subject_allowed(None));
}
#[test]
fn kind_str_roundtrip() {
for k in [
KnowledgeKind::VectorIndex,
KnowledgeKind::DocumentCorpus,
KnowledgeKind::IndexedDataset,
KnowledgeKind::Feed,
KnowledgeKind::EmbeddingStore,
KnowledgeKind::Other,
] {
assert_eq!(KnowledgeKind::parse_str(k.as_str()), Some(k));
}
assert_eq!(KnowledgeKind::parse_str("nonsense"), None);
}
}