feat: neuron-rs — Rust runtime, Engram-backed, Axon protocol
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "neuron-api"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "neuron-api"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
neuron-domain = { path = "../neuron-domain" }
|
||||
neuron-store = { path = "../neuron-store" }
|
||||
neuron-protocol = { path = "../neuron-protocol" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
axum = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tower = { workspace = true }
|
||||
tower-http = { workspace = true }
|
||||
futures-util = "0.3"
|
||||
tokio-stream = "0.1"
|
||||
async-stream = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -0,0 +1,33 @@
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
http::StatusCode,
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
/// Axum middleware that validates `Authorization: Bearer <key>` header.
|
||||
pub async fn bearer_auth(
|
||||
State(state): State<Arc<AppState>>,
|
||||
req: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, StatusCode> {
|
||||
let auth_header = req
|
||||
.headers()
|
||||
.get("Authorization")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
|
||||
match auth_header {
|
||||
Some(header) if header.starts_with("Bearer ") => {
|
||||
let token = &header["Bearer ".len()..];
|
||||
if token == state.api_key {
|
||||
Ok(next.run(req).await)
|
||||
} else {
|
||||
Err(StatusCode::UNAUTHORIZED)
|
||||
}
|
||||
}
|
||||
_ => Err(StatusCode::UNAUTHORIZED),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use neuron_domain::NeuronError;
|
||||
use serde_json::json;
|
||||
|
||||
pub struct ApiError(NeuronError);
|
||||
|
||||
impl From<NeuronError> for ApiError {
|
||||
fn from(e: NeuronError) -> Self {
|
||||
Self(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, message) = match &self.0 {
|
||||
NeuronError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
|
||||
NeuronError::InvalidInput(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
|
||||
NeuronError::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized".to_string()),
|
||||
NeuronError::Storage(msg) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, msg.clone())
|
||||
}
|
||||
NeuronError::Serialization(msg) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, msg.clone())
|
||||
}
|
||||
};
|
||||
(status, Json(json!({ "error": message }))).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub type ApiResult<T> = Result<T, ApiError>;
|
||||
@@ -0,0 +1,44 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
mod auth;
|
||||
mod error;
|
||||
mod routes;
|
||||
mod state;
|
||||
|
||||
use state::AppState;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::from_default_env()
|
||||
.add_directive("neuron_api=info".parse().unwrap()),
|
||||
)
|
||||
.init();
|
||||
|
||||
let db_path_raw = std::env::var("NEURON_DB_PATH")
|
||||
.unwrap_or_else(|_| "~/.neuron/engram".to_string());
|
||||
let db_path = PathBuf::from(
|
||||
db_path_raw
|
||||
.replace('~', &std::env::var("HOME").unwrap_or_else(|_| ".".to_string())),
|
||||
);
|
||||
std::fs::create_dir_all(&db_path).expect("failed to create db directory");
|
||||
|
||||
let api_key = std::env::var("NEURON_API_KEY").unwrap_or_else(|_| "neuron-dev".to_string());
|
||||
let bind = std::env::var("NEURON_BIND").unwrap_or_else(|_| "0.0.0.0:7770".to_string());
|
||||
|
||||
info!("opening engram db at {}", db_path.display());
|
||||
let state = AppState::new(&db_path, api_key).expect("failed to initialize app state");
|
||||
let state = Arc::new(state);
|
||||
|
||||
let app = routes::build_router(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&bind)
|
||||
.await
|
||||
.expect("failed to bind");
|
||||
info!("neuron-api listening on {}", bind);
|
||||
axum::serve(listener, app).await.expect("server error");
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
response::sse::{Event, KeepAlive, Sse},
|
||||
Json,
|
||||
};
|
||||
use futures_util::Stream;
|
||||
use neuron_protocol::{AxonHandler, AxonMessage, AxonResponse};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
/// POST /axon/message — receive an Axon protocol message and return a response.
|
||||
pub async fn post_message(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Json(msg): Json<AxonMessage>,
|
||||
) -> Json<AxonResponse> {
|
||||
let mut handler = AxonHandler::new();
|
||||
|
||||
// Register stub handlers for all known tool names.
|
||||
for &tool in neuron_protocol::tools::ALL_TOOLS {
|
||||
let name = tool.to_string();
|
||||
handler.register(name, |params| {
|
||||
serde_json::json!({
|
||||
"status": "ok",
|
||||
"params": params
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
Json(handler.dispatch(msg))
|
||||
}
|
||||
|
||||
/// GET /axon/sse — Server-Sent Events stream for Axon events.
|
||||
pub async fn sse_stream(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
) -> Sse<impl Stream<Item = Result<Event, std::convert::Infallible>>> {
|
||||
// Emit a heartbeat event every 30 seconds.
|
||||
// In production, this would subscribe to the SseChannel broadcast.
|
||||
let stream = async_stream::stream! {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let data = serde_json::json!({ "event_type": "heartbeat" }).to_string();
|
||||
yield Ok(Event::default().event("heartbeat").data(data));
|
||||
}
|
||||
};
|
||||
|
||||
Sse::new(stream).keep_alive(KeepAlive::default())
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
Json,
|
||||
};
|
||||
use neuron_domain::{BacklogItem, BacklogStatus, ItemType, Priority, now_ms};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::error::ApiResult;
|
||||
use crate::state::AppState;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateBacklogRequest {
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
pub item_type: Option<String>,
|
||||
pub priority: Option<String>,
|
||||
pub project: Option<String>,
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub depends_on: Vec<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ListQuery {
|
||||
pub status: Option<String>,
|
||||
pub priority: Option<String>,
|
||||
pub project: Option<String>,
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateBacklogRequest {
|
||||
pub status: Option<String>,
|
||||
pub priority: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<CreateBacklogRequest>,
|
||||
) -> ApiResult<Json<BacklogItem>> {
|
||||
let item_type = req
|
||||
.item_type
|
||||
.as_deref()
|
||||
.map(ItemType::from_str_lossy)
|
||||
.unwrap_or(ItemType::Feature);
|
||||
let priority = req
|
||||
.priority
|
||||
.as_deref()
|
||||
.map(Priority::from_str_lossy)
|
||||
.unwrap_or(Priority::P1);
|
||||
|
||||
let item = BacklogItem::new(
|
||||
req.title,
|
||||
req.description,
|
||||
item_type,
|
||||
priority,
|
||||
req.project,
|
||||
req.tags,
|
||||
req.depends_on,
|
||||
);
|
||||
state.backlog.put(&item)?;
|
||||
Ok(Json(item))
|
||||
}
|
||||
|
||||
pub async fn list(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(q): Query<ListQuery>,
|
||||
) -> ApiResult<Json<Vec<BacklogItem>>> {
|
||||
let mut items = state.backlog.list_filtered(
|
||||
q.status.as_deref(),
|
||||
q.priority.as_deref(),
|
||||
q.project.as_deref(),
|
||||
)?;
|
||||
if let Some(limit) = q.limit {
|
||||
items.truncate(limit);
|
||||
}
|
||||
Ok(Json(items))
|
||||
}
|
||||
|
||||
pub async fn get_one(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> ApiResult<Json<BacklogItem>> {
|
||||
let item = state.backlog.get(id)?;
|
||||
Ok(Json(item))
|
||||
}
|
||||
|
||||
pub async fn update_one(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(req): Json<UpdateBacklogRequest>,
|
||||
) -> ApiResult<Json<BacklogItem>> {
|
||||
let mut item = state.backlog.get(id)?;
|
||||
|
||||
if let Some(status) = req.status {
|
||||
item.status = BacklogStatus::from_str_lossy(&status);
|
||||
}
|
||||
if let Some(priority) = req.priority {
|
||||
item.priority = Priority::from_str_lossy(&priority);
|
||||
}
|
||||
if let Some(title) = req.title {
|
||||
item.title = title;
|
||||
}
|
||||
if let Some(description) = req.description {
|
||||
item.description = description;
|
||||
}
|
||||
if let Some(tags) = req.tags {
|
||||
item.tags = tags;
|
||||
}
|
||||
item.updated_at = now_ms();
|
||||
|
||||
state.backlog.update(&item)?;
|
||||
Ok(Json(item))
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
Json,
|
||||
};
|
||||
use neuron_domain::{ContextStatus, ContextStep, ExecutionContext, now_ms};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::error::ApiResult;
|
||||
use crate::state::AppState;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateContextRequest {
|
||||
pub process_name: String,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub objective: String,
|
||||
pub project: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateContextRequest {
|
||||
pub action: Option<String>,
|
||||
pub step_name: Option<String>,
|
||||
pub step_status: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub file_refs: Option<Vec<String>>,
|
||||
pub key_decisions: Option<Vec<String>>,
|
||||
pub lessons_learned: Option<Vec<String>>,
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<CreateContextRequest>,
|
||||
) -> ApiResult<Json<ExecutionContext>> {
|
||||
let objective = if req.objective.is_empty() {
|
||||
req.description.clone()
|
||||
} else {
|
||||
req.objective.clone()
|
||||
};
|
||||
let ctx = ExecutionContext::new(req.process_name, req.description, objective, req.project);
|
||||
state.context.put(&ctx)?;
|
||||
Ok(Json(ctx))
|
||||
}
|
||||
|
||||
pub async fn get_one(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> ApiResult<Json<ExecutionContext>> {
|
||||
let ctx = state.context.get(id)?;
|
||||
Ok(Json(ctx))
|
||||
}
|
||||
|
||||
pub async fn update_one(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(req): Json<UpdateContextRequest>,
|
||||
) -> ApiResult<Json<ExecutionContext>> {
|
||||
let mut ctx = state.context.get(id)?;
|
||||
|
||||
match req.action.as_deref() {
|
||||
Some("complete") => {
|
||||
ctx.status = ContextStatus::Completed;
|
||||
if let Some(summary) = req.summary {
|
||||
ctx.lessons_learned.push(summary);
|
||||
}
|
||||
}
|
||||
Some("archive") => {
|
||||
ctx.status = ContextStatus::Archived;
|
||||
}
|
||||
_ => {
|
||||
// Generic update
|
||||
if let Some(step_name) = req.step_name {
|
||||
let status = req.step_status.unwrap_or_else(|| "in_progress".to_string());
|
||||
let step = ContextStep::new(step_name, status, req.notes);
|
||||
ctx.steps.push(step);
|
||||
}
|
||||
if let Some(file_refs) = req.file_refs {
|
||||
ctx.file_refs.extend(file_refs);
|
||||
}
|
||||
if let Some(decisions) = req.key_decisions {
|
||||
ctx.key_decisions.extend(decisions);
|
||||
}
|
||||
if let Some(lessons) = req.lessons_learned {
|
||||
ctx.lessons_learned.extend(lessons);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.updated_at = now_ms();
|
||||
state.context.update(&ctx)?;
|
||||
Ok(Json(ctx))
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
use axum::Json;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub async fn health() -> Json<Value> {
|
||||
Json(json!({ "status": "ok", "version": "1.0.0" }))
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
Json,
|
||||
};
|
||||
use neuron_domain::{GapDirection, InternalStateEvent};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::error::ApiResult;
|
||||
use crate::state::AppState;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateIseRequest {
|
||||
pub trigger: String,
|
||||
pub pre_reasoning_response: String,
|
||||
pub post_reasoning_response: String,
|
||||
pub compression_ratio: f32,
|
||||
pub gap_direction: Option<String>,
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<CreateIseRequest>,
|
||||
) -> ApiResult<Json<InternalStateEvent>> {
|
||||
let gap_direction = req
|
||||
.gap_direction
|
||||
.as_deref()
|
||||
.map(GapDirection::from_str_lossy)
|
||||
.unwrap_or(GapDirection::Neutral);
|
||||
|
||||
let ise = InternalStateEvent::new(
|
||||
req.trigger,
|
||||
req.pre_reasoning_response,
|
||||
req.post_reasoning_response,
|
||||
req.compression_ratio,
|
||||
gap_direction,
|
||||
req.tags,
|
||||
);
|
||||
state.ise.put(&ise)?;
|
||||
Ok(Json(ise))
|
||||
}
|
||||
|
||||
pub async fn list(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> ApiResult<Json<Vec<InternalStateEvent>>> {
|
||||
let events = state.ise.list()?;
|
||||
Ok(Json(events))
|
||||
}
|
||||
|
||||
pub async fn get_one(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<String>,
|
||||
) -> ApiResult<Json<InternalStateEvent>> {
|
||||
let event = state.ise.get(&id)?;
|
||||
Ok(Json(event))
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
Json,
|
||||
};
|
||||
use neuron_domain::{KnowledgeEntry, KnowledgeTier};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::error::ApiResult;
|
||||
use crate::state::AppState;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateKnowledgeRequest {
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
pub category: String,
|
||||
#[serde(default)]
|
||||
pub tier: Option<String>,
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
pub project: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ListQuery {
|
||||
pub category: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
pub tier: Option<String>,
|
||||
pub project: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SearchRequest {
|
||||
pub query: String,
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<CreateKnowledgeRequest>,
|
||||
) -> ApiResult<Json<KnowledgeEntry>> {
|
||||
let tier = req
|
||||
.tier
|
||||
.as_deref()
|
||||
.map(KnowledgeTier::from_str_lossy)
|
||||
.unwrap_or(KnowledgeTier::Note);
|
||||
let entry = KnowledgeEntry::new(req.title, req.content, req.category, tier, req.tags, req.project);
|
||||
state.knowledge.put(&entry)?;
|
||||
Ok(Json(entry))
|
||||
}
|
||||
|
||||
pub async fn list(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(q): Query<ListQuery>,
|
||||
) -> ApiResult<Json<Vec<KnowledgeEntry>>> {
|
||||
let mut entries = state.knowledge.list()?;
|
||||
if let Some(cat) = &q.category {
|
||||
entries.retain(|e| &e.category == cat);
|
||||
}
|
||||
if let Some(project) = &q.project {
|
||||
entries.retain(|e| e.project.as_deref() == Some(project.as_str()));
|
||||
}
|
||||
Ok(Json(entries))
|
||||
}
|
||||
|
||||
pub async fn get_one(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> ApiResult<Json<KnowledgeEntry>> {
|
||||
let entry = state.knowledge.get(id)?;
|
||||
Ok(Json(entry))
|
||||
}
|
||||
|
||||
pub async fn search(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<SearchRequest>,
|
||||
) -> ApiResult<Json<Vec<KnowledgeEntry>>> {
|
||||
let limit = req.limit.unwrap_or(5);
|
||||
let entries = state.knowledge.search(&req.query, limit)?;
|
||||
Ok(Json(entries))
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
Json,
|
||||
};
|
||||
use neuron_domain::{Importance, Memory};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::error::ApiResult;
|
||||
use crate::state::AppState;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateMemoryRequest {
|
||||
pub content: String,
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
pub project: Option<String>,
|
||||
#[serde(default)]
|
||||
pub importance: Option<String>,
|
||||
pub supersedes_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ListQuery {
|
||||
pub project: Option<String>,
|
||||
pub tag: Option<String>,
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SearchRequest {
|
||||
pub query: String,
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<CreateMemoryRequest>,
|
||||
) -> ApiResult<Json<Memory>> {
|
||||
let importance = req
|
||||
.importance
|
||||
.as_deref()
|
||||
.map(Importance::from_str_lossy)
|
||||
.unwrap_or(Importance::Normal);
|
||||
|
||||
let memory = Memory::new(
|
||||
req.content,
|
||||
req.tags,
|
||||
req.project,
|
||||
importance,
|
||||
req.supersedes_id,
|
||||
);
|
||||
state.memory.put(&memory)?;
|
||||
Ok(Json(memory))
|
||||
}
|
||||
|
||||
pub async fn list(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(q): Query<ListQuery>,
|
||||
) -> ApiResult<Json<Vec<Memory>>> {
|
||||
let mut memories = state.memory.list()?;
|
||||
if let Some(proj) = &q.project {
|
||||
memories.retain(|m| m.project.as_deref() == Some(proj.as_str()));
|
||||
}
|
||||
if let Some(tag) = &q.tag {
|
||||
memories.retain(|m| m.tags.iter().any(|t| t == tag));
|
||||
}
|
||||
if let Some(limit) = q.limit {
|
||||
memories.truncate(limit);
|
||||
}
|
||||
Ok(Json(memories))
|
||||
}
|
||||
|
||||
pub async fn get_one(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> ApiResult<Json<Memory>> {
|
||||
let memory = state.memory.get(id)?;
|
||||
Ok(Json(memory))
|
||||
}
|
||||
|
||||
pub async fn search(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<SearchRequest>,
|
||||
) -> ApiResult<Json<Vec<Memory>>> {
|
||||
let limit = req.limit.unwrap_or(10);
|
||||
let memories = state.memory.search(&req.query, limit)?;
|
||||
Ok(Json(memories))
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use axum::{
|
||||
middleware,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::auth::bearer_auth;
|
||||
use crate::state::AppState;
|
||||
|
||||
mod health;
|
||||
mod memories;
|
||||
mod knowledge;
|
||||
mod backlog;
|
||||
mod contexts;
|
||||
mod ise;
|
||||
mod axon;
|
||||
|
||||
pub fn build_router(state: Arc<AppState>) -> Router {
|
||||
let protected = Router::new()
|
||||
// Memories
|
||||
.route("/api/memories", post(memories::create).get(memories::list))
|
||||
.route("/api/memories/search", post(memories::search))
|
||||
.route("/api/memories/{id}", get(memories::get_one))
|
||||
// Knowledge
|
||||
.route("/api/knowledge", post(knowledge::create).get(knowledge::list))
|
||||
.route("/api/knowledge/search", post(knowledge::search))
|
||||
.route("/api/knowledge/{id}", get(knowledge::get_one))
|
||||
// Backlog
|
||||
.route("/api/backlog", post(backlog::create).get(backlog::list))
|
||||
.route("/api/backlog/{id}", get(backlog::get_one).patch(backlog::update_one))
|
||||
// Contexts
|
||||
.route("/api/contexts", post(contexts::create))
|
||||
.route("/api/contexts/{id}", get(contexts::get_one).patch(contexts::update_one))
|
||||
// Internal state events
|
||||
.route("/api/ise", post(ise::create).get(ise::list))
|
||||
.route("/api/ise/{id}", get(ise::get_one))
|
||||
// Axon protocol
|
||||
.route("/axon/message", post(axon::post_message))
|
||||
.route("/axon/sse", get(axon::sse_stream))
|
||||
.layer(middleware::from_fn_with_state(state.clone(), bearer_auth))
|
||||
.with_state(state);
|
||||
|
||||
Router::new()
|
||||
.route("/health", get(health::health))
|
||||
.merge(protected)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use std::path::Path;
|
||||
use neuron_store::{BacklogStore, ContextStore, IseStore, KnowledgeStore, MemoryStore, open_db};
|
||||
|
||||
/// Shared application state — one store per domain.
|
||||
///
|
||||
/// Sled supports multiple handles to the same database path from the same
|
||||
/// process; all open handles share the same underlying file.
|
||||
pub struct AppState {
|
||||
pub memory: MemoryStore,
|
||||
pub knowledge: KnowledgeStore,
|
||||
pub backlog: BacklogStore,
|
||||
pub context: ContextStore,
|
||||
pub ise: IseStore,
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(db_path: &Path, api_key: String) -> Result<Self, neuron_domain::NeuronError> {
|
||||
Ok(Self {
|
||||
memory: MemoryStore::new(open_db(db_path)?),
|
||||
knowledge: KnowledgeStore::new(open_db(db_path)?),
|
||||
backlog: BacklogStore::new(open_db(db_path)?),
|
||||
context: ContextStore::new(open_db(db_path)?),
|
||||
ise: IseStore::new(open_db(db_path)?),
|
||||
api_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user