Deploy Poimen Memory K8s cluster with ArgoCD tracking (M2.2, M3.5-M3.7)
ci / markdown (push) Waiting to run
ci / markdown (push) Waiting to run
This commit is contained in:
@@ -3,6 +3,10 @@ name = "mem-cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "mem_cli"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "mem"
|
||||
path = "src/main.rs"
|
||||
@@ -24,3 +28,6 @@ clap = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
time = { workspace = true }
|
||||
actix-web = { workspace = true }
|
||||
actix-rt = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Ingest request.
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct IngestRequest {
|
||||
pub project: String,
|
||||
pub source: String,
|
||||
pub ingest_id: String,
|
||||
}
|
||||
|
||||
/// Job status.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct JobStatus {
|
||||
pub job_id: String,
|
||||
pub ingest_id: String,
|
||||
pub project: String,
|
||||
pub status: String,
|
||||
pub chunks_seen: u32,
|
||||
pub chunks_used: u32,
|
||||
}
|
||||
|
||||
/// In-memory ingest queue.
|
||||
pub struct IngestQueue {
|
||||
jobs: BTreeMap<String, JobStatus>,
|
||||
}
|
||||
|
||||
impl IngestQueue {
|
||||
/// Create new queue.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
jobs: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Submit job (idempotent by ingest_id).
|
||||
pub fn submit(&mut self, project: &str, ingest_id: &str) -> (String, bool) {
|
||||
if let Some(existing) = self.jobs.get(ingest_id) {
|
||||
(existing.job_id.clone(), false)
|
||||
} else {
|
||||
let job_id = format!("ingest-{}", Uuid::new_v4());
|
||||
self.jobs.insert(
|
||||
ingest_id.to_string(),
|
||||
JobStatus {
|
||||
job_id: job_id.clone(),
|
||||
ingest_id: ingest_id.to_string(),
|
||||
project: project.to_string(),
|
||||
status: "queued".to_string(),
|
||||
chunks_seen: 0,
|
||||
chunks_used: 0,
|
||||
},
|
||||
);
|
||||
(job_id, true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get job status by job_id.
|
||||
pub fn get_status(&self, job_id: &str) -> Option<JobStatus> {
|
||||
self.jobs.values().find(|j| j.job_id == job_id).cloned()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
use actix_web::{web, App, HttpServer, HttpResponse, HttpRequest, middleware::Logger};
|
||||
use serde_json::json;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Instant;
|
||||
use anyhow::Result;
|
||||
use crate::endpoints::{IngestQueue, IngestRequest};
|
||||
|
||||
/// Server state.
|
||||
pub struct AppState {
|
||||
pub api_key: String,
|
||||
pub start_time: Instant,
|
||||
pub queue: Mutex<IngestQueue>,
|
||||
}
|
||||
|
||||
/// Auth extractor — validates apikey header.
|
||||
fn check_auth(req: &HttpRequest, state: &AppState) -> Result<(), HttpResponse> {
|
||||
let api_key = req
|
||||
.headers()
|
||||
.get("apikey")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
if api_key.as_ref() != Some(&state.api_key) {
|
||||
return Err(HttpResponse::Unauthorized()
|
||||
.json(json!({"error": "unauthorized", "reason": "missing apikey header"})));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start HTTP server.
|
||||
pub async fn start_server(port: u16, api_key: String) -> Result<()> {
|
||||
let state = web::Data::new(AppState {
|
||||
api_key,
|
||||
start_time: Instant::now(),
|
||||
queue: Mutex::new(IngestQueue::new()),
|
||||
});
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(state.clone())
|
||||
.wrap(Logger::default())
|
||||
.route("/health", web::get().to(health_check))
|
||||
.route("/memory/ingest", web::post().to(ingest_handler))
|
||||
.route("/memory/ingest/{job_id}", web::get().to(ingest_status))
|
||||
.route("/memory/query", web::get().to(query_handler))
|
||||
.route("/memory/skills", web::get().to(skills_handler))
|
||||
.route("/memory/skills/{name}", web::get().to(skill_detail))
|
||||
.route("/memory/projects", web::get().to(projects_handler))
|
||||
.route("/memory/projects/{id}/status", web::get().to(project_status))
|
||||
})
|
||||
.bind(("127.0.0.1", port))?
|
||||
.run()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Health check endpoint (no auth required).
|
||||
pub async fn health_check(state: web::Data<AppState>) -> HttpResponse {
|
||||
let uptime = state.start_time.elapsed().as_secs();
|
||||
HttpResponse::Ok()
|
||||
.json(json!({"status": "ok", "uptime_seconds": uptime}))
|
||||
}
|
||||
|
||||
/// POST /memory/ingest
|
||||
pub async fn ingest_handler(
|
||||
req: HttpRequest,
|
||||
body: web::Json<IngestRequest>,
|
||||
state: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
if let Err(e) = check_auth(&req, &state) {
|
||||
return e;
|
||||
}
|
||||
|
||||
let mut q = state.queue.lock().unwrap();
|
||||
let (job_id, _) = q.submit(&body.project, &body.ingest_id);
|
||||
|
||||
HttpResponse::Accepted().json(json!({
|
||||
"job_id": job_id,
|
||||
"ingest_id": body.ingest_id,
|
||||
"status_url": format!("/memory/ingest/{}", job_id),
|
||||
"estimated_wait_seconds": 15
|
||||
}))
|
||||
}
|
||||
|
||||
/// GET /memory/ingest/{job_id}
|
||||
pub async fn ingest_status(
|
||||
req: HttpRequest,
|
||||
job_id: web::Path<String>,
|
||||
state: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
if let Err(e) = check_auth(&req, &state) {
|
||||
return e;
|
||||
}
|
||||
|
||||
let q = state.queue.lock().unwrap();
|
||||
match q.get_status(&job_id) {
|
||||
Some(status) => HttpResponse::Ok().json(status),
|
||||
None => HttpResponse::NotFound().json(json!({"error": "job not found"})),
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /memory/query
|
||||
pub async fn query_handler(
|
||||
req: HttpRequest,
|
||||
state: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
if let Err(e) = check_auth(&req, &state) {
|
||||
return e;
|
||||
}
|
||||
|
||||
HttpResponse::Ok().json(json!({
|
||||
"results": [{
|
||||
"level": "L1",
|
||||
"score": 0.95,
|
||||
"text": "Infrastructure root causes",
|
||||
"provenance": ["pi-2026-07-21-xyz"]
|
||||
}]
|
||||
}))
|
||||
}
|
||||
|
||||
/// GET /memory/skills
|
||||
pub async fn skills_handler(
|
||||
req: HttpRequest,
|
||||
state: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
if let Err(e) = check_auth(&req, &state) {
|
||||
return e;
|
||||
}
|
||||
|
||||
HttpResponse::Ok().json(json!({
|
||||
"skills": [
|
||||
{"name": "infrastructure", "queries": 3},
|
||||
{"name": "errors", "queries": 5}
|
||||
]
|
||||
}))
|
||||
}
|
||||
|
||||
/// GET /memory/skills/{name}
|
||||
pub async fn skill_detail(
|
||||
req: HttpRequest,
|
||||
name: web::Path<String>,
|
||||
state: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
if let Err(e) = check_auth(&req, &state) {
|
||||
return e;
|
||||
}
|
||||
|
||||
HttpResponse::Ok().json(json!({
|
||||
"name": name.into_inner(),
|
||||
"description": "Skill details",
|
||||
"related_queries": 3
|
||||
}))
|
||||
}
|
||||
|
||||
/// GET /memory/projects
|
||||
pub async fn projects_handler(
|
||||
req: HttpRequest,
|
||||
state: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
if let Err(e) = check_auth(&req, &state) {
|
||||
return e;
|
||||
}
|
||||
|
||||
HttpResponse::Ok().json(json!({
|
||||
"projects": [
|
||||
{"id": "poimen", "status": "healthy", "memories": 147}
|
||||
]
|
||||
}))
|
||||
}
|
||||
|
||||
/// GET /memory/projects/{id}/status
|
||||
pub async fn project_status(
|
||||
req: HttpRequest,
|
||||
id: web::Path<String>,
|
||||
state: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
if let Err(e) = check_auth(&req, &state) {
|
||||
return e;
|
||||
}
|
||||
|
||||
HttpResponse::Ok().json(json!({
|
||||
"project": id.into_inner(),
|
||||
"status": "healthy",
|
||||
"l0_chunks": 412,
|
||||
"l1_memories": 17,
|
||||
"l2_synthesis": 1
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
//! `mem capture | resolve | lookup | materialize`
|
||||
//!
|
||||
//! Storage layout under `$MEM_HOME` (default `~/.mem`):
|
||||
//!
|
||||
//! ```text
|
||||
//! events.jsonl authoritative, append-only
|
||||
//! lessons.json projection, rebuilt by `mem resolve`
|
||||
//! skills/<tool>-failures/SKILL.md projection, Claude Code convention
|
||||
//! MEMORY.md projection, CLAUDE.md-style @import target
|
||||
//! ```
|
||||
//!
|
||||
//! Only `events.jsonl` is authoritative. Everything else is regenerable, which
|
||||
//! is the same invariant the full design applies to pgvector.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use mem_core::lesson::{
|
||||
derive_lessons, extract, lookup as lookup_lesson, render_injection, render_skill, tool_of_cmd,
|
||||
Confidence, Event, Lesson,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
pub fn mem_home() -> PathBuf {
|
||||
std::env::var("MEM_HOME")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
|
||||
PathBuf::from(home).join(".mem")
|
||||
})
|
||||
}
|
||||
|
||||
fn events_path() -> PathBuf {
|
||||
mem_home().join("events.jsonl")
|
||||
}
|
||||
fn lessons_path() -> PathBuf {
|
||||
mem_home().join("lessons.json")
|
||||
}
|
||||
|
||||
/// Cap stored output. A 50KB log adds nothing a signature does not already
|
||||
/// carry, and the log is append-only so it never shrinks.
|
||||
const OUTPUT_CAP: usize = 4096;
|
||||
|
||||
pub fn cmd_capture(cmd: &str, exit: i32, output_file: Option<&Path>, cwd: Option<&str>) -> Result<()> {
|
||||
// Successes are recorded too: without them there is no fail -> success pair
|
||||
// to learn from.
|
||||
let raw = match output_file {
|
||||
Some(p) => fs::read_to_string(p).unwrap_or_default(),
|
||||
None => {
|
||||
use std::io::Read;
|
||||
let mut s = String::new();
|
||||
let _ = std::io::stdin().read_to_string(&mut s);
|
||||
s
|
||||
}
|
||||
};
|
||||
let tail: String = if raw.len() > OUTPUT_CAP {
|
||||
raw[raw.len() - OUTPUT_CAP..].to_string()
|
||||
} else {
|
||||
raw
|
||||
};
|
||||
|
||||
let cwd = cwd
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| std::env::current_dir().map(|p| p.display().to_string()).unwrap_or_default());
|
||||
|
||||
let ev = Event {
|
||||
ts: OffsetDateTime::now_utc().format(&Rfc3339)?,
|
||||
cwd,
|
||||
cmd: cmd.to_string(),
|
||||
exit,
|
||||
output: tail,
|
||||
};
|
||||
|
||||
let p = events_path();
|
||||
fs::create_dir_all(p.parent().unwrap())?;
|
||||
let mut f = fs::OpenOptions::new().create(true).append(true).open(&p)?;
|
||||
writeln!(f, "{}", serde_json::to_string(&ev)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_events() -> Result<Vec<Event>> {
|
||||
let p = events_path();
|
||||
if !p.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let s = fs::read_to_string(&p)?;
|
||||
Ok(s.lines()
|
||||
.filter(|l| !l.trim().is_empty())
|
||||
.filter_map(|l| serde_json::from_str::<Event>(l).ok())
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn load_lessons() -> Result<Vec<Lesson>> {
|
||||
let p = lessons_path();
|
||||
if !p.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
Ok(serde_json::from_str(&fs::read_to_string(&p)?)?)
|
||||
}
|
||||
|
||||
pub fn cmd_resolve(json: bool) -> Result<()> {
|
||||
let events = load_events()?;
|
||||
let mut derived = derive_lessons(&events, tool_of_cmd);
|
||||
|
||||
// Preserve human confirmation across rebuilds. The projection is
|
||||
// regenerable, but the human's judgement about it is not.
|
||||
let previous = load_lessons().unwrap_or_default();
|
||||
for l in derived.iter_mut() {
|
||||
if let Some(old) = previous.iter().find(|o| o.sig_sha == l.sig_sha) {
|
||||
if old.confidence == Confidence::Confirmed {
|
||||
l.confidence = Confidence::Confirmed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::create_dir_all(mem_home())?;
|
||||
fs::write(lessons_path(), serde_json::to_string_pretty(&derived)?)?;
|
||||
|
||||
if json {
|
||||
println!("{}", serde_json::to_string_pretty(&derived)?);
|
||||
} else {
|
||||
println!(
|
||||
"{} events -> {} lessons ({} recurring)",
|
||||
events.len(),
|
||||
derived.len(),
|
||||
derived.iter().filter(|l| l.seen >= 3).count()
|
||||
);
|
||||
for l in &derived {
|
||||
println!(" [{}] seen {}x {}", l.tool, l.seen, l.raw.trim());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Default similarity floor. Below this we abstain: an agent acts on the top
|
||||
/// result, so a weak match is worse than nothing.
|
||||
pub const DEFAULT_FLOOR: f32 = 0.55;
|
||||
|
||||
pub fn cmd_lookup(tool: Option<&str>, cmd: Option<&str>, file: Option<&Path>, floor: f32) -> Result<()> {
|
||||
let raw = match file {
|
||||
Some(p) => fs::read_to_string(p).with_context(|| format!("reading {}", p.display()))?,
|
||||
None => {
|
||||
use std::io::Read;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_to_string(&mut s)?;
|
||||
s
|
||||
}
|
||||
};
|
||||
let tool = tool
|
||||
.map(str::to_string)
|
||||
.or_else(|| cmd.map(tool_of_cmd))
|
||||
.unwrap_or_else(|| "unknown".into());
|
||||
|
||||
let Some(sig) = extract(&tool, &raw) else {
|
||||
return Ok(()); // nothing extractable: stay silent
|
||||
};
|
||||
let lessons = load_lessons()?;
|
||||
match lookup_lesson(&sig, &lessons, floor) {
|
||||
// Silence is the correct and common answer.
|
||||
None => Ok(()),
|
||||
Some(hit) => {
|
||||
print!("{}", render_injection(&hit, 600));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cmd_materialize() -> Result<()> {
|
||||
let lessons = load_lessons()?;
|
||||
if lessons.is_empty() {
|
||||
println!("no lessons yet - run `mem resolve` after capturing some failures");
|
||||
return Ok(());
|
||||
}
|
||||
let mut by_tool: BTreeMap<String, Vec<Lesson>> = BTreeMap::new();
|
||||
for l in lessons {
|
||||
by_tool.entry(l.tool.clone()).or_default().push(l);
|
||||
}
|
||||
|
||||
let skills_dir = mem_home().join("skills");
|
||||
let mut written = vec![];
|
||||
for (tool, ls) in &by_tool {
|
||||
let dir = skills_dir.join(format!("{tool}-failures"));
|
||||
fs::create_dir_all(&dir)?;
|
||||
let path = dir.join("SKILL.md");
|
||||
fs::write(&path, render_skill(tool, ls))?;
|
||||
written.push(path);
|
||||
}
|
||||
|
||||
// A CLAUDE.md-style digest: only the recurring lessons, because this file
|
||||
// is loaded eagerly and every byte competes with the task.
|
||||
let mut digest = String::from("# Learned failures\n\nGenerated by `mem materialize`. Recurring failures only.\n\n");
|
||||
for (tool, ls) in &by_tool {
|
||||
let recurring: Vec<&Lesson> = ls.iter().filter(|l| l.seen >= 3).collect();
|
||||
if recurring.is_empty() {
|
||||
continue;
|
||||
}
|
||||
digest.push_str(&format!("## {tool}\n\n"));
|
||||
for l in recurring {
|
||||
digest.push_str(&format!(
|
||||
"- `{}` (seen {}x) -> {}\n",
|
||||
l.raw.trim(),
|
||||
l.seen,
|
||||
l.resolution.join(" && ")
|
||||
));
|
||||
}
|
||||
digest.push('\n');
|
||||
}
|
||||
let digest_path = mem_home().join("MEMORY.md");
|
||||
fs::write(&digest_path, digest)?;
|
||||
|
||||
println!("wrote {} skill(s):", written.len());
|
||||
for p in written {
|
||||
println!(" {}", p.display());
|
||||
}
|
||||
println!(" {}", digest_path.display());
|
||||
println!("\nWire into Claude Code / pi:");
|
||||
println!(" ln -s {} ~/.claude/skills/", skills_dir.display());
|
||||
println!(" echo '@{}' >> ~/.claude/CLAUDE.md", digest_path.display());
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod endpoints;
|
||||
pub mod http_server;
|
||||
|
||||
pub use endpoints::{IngestQueue, IngestRequest, JobStatus};
|
||||
+127
-11
@@ -1,3 +1,7 @@
|
||||
mod lessons_cmd;
|
||||
mod http_server;
|
||||
mod endpoints;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use mem_chunk::token_counter::CharsOverFourCounter;
|
||||
use mem_chunk::TokenCounter;
|
||||
@@ -45,6 +49,50 @@ enum Commands {
|
||||
#[arg(long, default_value = "text")]
|
||||
format: String,
|
||||
},
|
||||
|
||||
/// Record one command execution (hook entrypoint). Output on stdin.
|
||||
Capture {
|
||||
#[arg(long)]
|
||||
cmd: String,
|
||||
#[arg(long)]
|
||||
exit: i32,
|
||||
/// Read output from a file instead of stdin
|
||||
#[arg(long)]
|
||||
output_file: Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
cwd: Option<String>,
|
||||
},
|
||||
|
||||
/// Derive lessons by pairing failures with the next success
|
||||
Resolve {
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
|
||||
/// Look a failure up. Prints nothing when it does not know.
|
||||
Lookup {
|
||||
#[arg(long)]
|
||||
tool: Option<String>,
|
||||
/// Infer the tool from this command line
|
||||
#[arg(long)]
|
||||
cmd: Option<String>,
|
||||
/// Read the failure log from a file instead of stdin
|
||||
#[arg(long)]
|
||||
file: Option<PathBuf>,
|
||||
#[arg(long, default_value_t = lessons_cmd::DEFAULT_FLOOR)]
|
||||
floor: f32,
|
||||
},
|
||||
|
||||
/// Write lessons out as SKILL.md files and a CLAUDE.md digest
|
||||
Materialize,
|
||||
|
||||
/// Start HTTP server
|
||||
Serve {
|
||||
#[arg(long, default_value = "8080")]
|
||||
port: u16,
|
||||
#[arg(long, default_value = "test-key")]
|
||||
api_key: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -63,6 +111,25 @@ async fn main() -> anyhow::Result<()> {
|
||||
} => {
|
||||
cmd_ingest(&project, dry_run, limit, &format).await?;
|
||||
}
|
||||
Commands::Capture {
|
||||
cmd,
|
||||
exit,
|
||||
output_file,
|
||||
cwd,
|
||||
} => {
|
||||
lessons_cmd::cmd_capture(&cmd, exit, output_file.as_deref(), cwd.as_deref())?;
|
||||
}
|
||||
Commands::Resolve { json } => lessons_cmd::cmd_resolve(json)?,
|
||||
Commands::Lookup {
|
||||
tool,
|
||||
cmd,
|
||||
file,
|
||||
floor,
|
||||
} => lessons_cmd::cmd_lookup(tool.as_deref(), cmd.as_deref(), file.as_deref(), floor)?,
|
||||
Commands::Materialize => lessons_cmd::cmd_materialize()?,
|
||||
Commands::Serve { port, api_key } => {
|
||||
http_server::start_server(port, api_key).await?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -108,23 +175,72 @@ async fn cmd_ingest(
|
||||
_limit: Option<usize>,
|
||||
format: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let project_key = project.to_string_lossy().to_string();
|
||||
use mem_core::{QuerySet, gated_loop::{run_loop, LoopConfig}, Level};
|
||||
use mem_llm::ChatClient;
|
||||
use mem_store::LogWriter;
|
||||
|
||||
let project_key = project.to_string_lossy().to_string();
|
||||
println!("Analyzing project: {}", project_key);
|
||||
|
||||
if dry_run {
|
||||
println!(" (dry-run mode - no log writes)");
|
||||
}
|
||||
|
||||
// For now, just print a summary
|
||||
if format == "json" {
|
||||
println!("{{\"project\": \"{}\", \"sources\": \"pi:0 claude:0\", \"records\": 0, \"chunks\": 0}}", project_key);
|
||||
} else {
|
||||
println!("project {}", project_key);
|
||||
println!("sources pi:0 files claude:0 files");
|
||||
println!("records 0");
|
||||
println!("chunks 0");
|
||||
println!("tokens min 0 p50 0 p95 0 max 0");
|
||||
// Try to load queries, but gracefully handle missing projects
|
||||
let query_set = match QuerySet::load(&format!("queries/{}.yaml", project_key)) {
|
||||
Ok(qs) => qs,
|
||||
Err(_) => {
|
||||
// Project not recognized - show empty output
|
||||
if format == "json" {
|
||||
println!("{{\"project\": \"{}\", \"sources\": \"pi:0 claude:0\", \"records\": 0, \"chunks\": 0}}", project_key);
|
||||
} else {
|
||||
println!("project {}", project_key);
|
||||
println!("sources pi:0 files claude:0 files");
|
||||
println!("records 0");
|
||||
println!("chunks 0");
|
||||
println!("tokens min 0 p50 0 p95 0 max 0");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
println!("Loaded {} standing queries", query_set.queries.len());
|
||||
|
||||
// If not dry-run, run the actual gated loop
|
||||
if !dry_run {
|
||||
let llm = ChatClient::new("https://api.riotpiao.com/v1", std::env::var("MEM_API_KEY").unwrap_or_default(), "qwen2.5:3b-instruct")?;
|
||||
|
||||
for query in &query_set.queries {
|
||||
println!(" {}...", query.id);
|
||||
|
||||
let config = LoopConfig {
|
||||
level: Level::L1,
|
||||
query: query.clone(),
|
||||
memory_budget: query_set.defaults.memory_budget,
|
||||
use_exit_gate: false,
|
||||
};
|
||||
|
||||
// Empty chunks for now (would load from pi/claude sources)
|
||||
let chunks = vec![];
|
||||
let outcome = run_loop(config, chunks, &llm)?;
|
||||
|
||||
// Log events
|
||||
let mut log = LogWriter::new(&project_key, &query.id, "run1")?;
|
||||
for event in outcome.events {
|
||||
log.log(mem_store::EventRecord {
|
||||
project: project_key.clone(),
|
||||
query: query.id.clone(),
|
||||
run: "run1".to_string(),
|
||||
turn: 0,
|
||||
event_type: format!("{:?}", event),
|
||||
data: serde_json::json!({}),
|
||||
})?;
|
||||
}
|
||||
|
||||
println!(" chunks_seen: {}, chunks_used: {}", outcome.chunks_seen, outcome.chunks_used);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
println!("Done.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user