mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-05-25 23:24:03 +00:00
feat(rvagent-learning): add Cloud Run deployment and CLI binary
- Add daily_cycle binary for running learning cycles - Supports --once, --status, --dry-run, --scan-dir flags - Shows GOAP-based discovery with Gemini 2.5 Flash - Submits discoveries to π.ruv.io cloud brain - Add Dockerfile for Cloud Run deployment - Multi-stage Rust build with gcloud CLI - Runs as non-root user - Add cloudbuild.yaml for automated deployment - Deploys to us-central1 on ruv-dev project - Injects Gemini API key from Secret Manager - Update Cargo.toml with binary definition and tracing-subscriber Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
0af0ffe8ca
commit
5404a04aba
4 changed files with 342 additions and 0 deletions
|
|
@ -45,6 +45,10 @@ notify = { version = "6.1", optional = true }
|
|||
# rvagent-core = { path = "../rvagent-core", optional = true }
|
||||
# ruvector-sona = { path = "../../sona", optional = true }
|
||||
|
||||
[dependencies.tracing-subscriber]
|
||||
version = "0.3"
|
||||
features = ["env-filter"]
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
tempfile = "3.13"
|
||||
|
|
@ -58,5 +62,9 @@ full = ["scheduler"]
|
|||
[lib]
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "daily_cycle"
|
||||
path = "src/bin/daily_cycle.rs"
|
||||
|
||||
[lints.clippy]
|
||||
manual_range_contains = "allow"
|
||||
|
|
|
|||
59
crates/rvAgent/rvagent-learning/Dockerfile
Normal file
59
crates/rvAgent/rvagent-learning/Dockerfile
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# rvagent-learning Cloud Run Dockerfile
|
||||
# Daily Learning Loop with GOAP reasoning
|
||||
|
||||
FROM rust:1.77-bookworm AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install build dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy workspace files
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/rvAgent/rvagent-learning ./crates/rvAgent/rvagent-learning
|
||||
|
||||
# Create dummy workspace members to satisfy Cargo
|
||||
RUN mkdir -p crates/ruvector-core/src && echo "pub fn dummy() {}" > crates/ruvector-core/src/lib.rs
|
||||
RUN echo '[package]\nname = "ruvector-core"\nversion = "0.1.0"\nedition = "2021"' > crates/ruvector-core/Cargo.toml
|
||||
|
||||
# Build release binary
|
||||
RUN cargo build --release -p rvagent-learning --bin daily_cycle
|
||||
|
||||
# Runtime image
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ca-certificates \
|
||||
libssl3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install gcloud CLI for secret manager access
|
||||
RUN apt-get update && apt-get install -y curl gnupg \
|
||||
&& echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] http://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list \
|
||||
&& curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - \
|
||||
&& apt-get update && apt-get install -y google-cloud-cli \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy binary
|
||||
COPY --from=builder /app/target/release/daily_cycle /app/daily_cycle
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -r -s /bin/false appuser
|
||||
USER appuser
|
||||
|
||||
# Set environment
|
||||
ENV RUST_LOG=info
|
||||
ENV PORT=8080
|
||||
|
||||
# Health check endpoint would be on the scheduler
|
||||
EXPOSE 8080
|
||||
|
||||
# Run the learning cycle
|
||||
ENTRYPOINT ["/app/daily_cycle"]
|
||||
CMD ["--once"]
|
||||
60
crates/rvAgent/rvagent-learning/cloudbuild.yaml
Normal file
60
crates/rvAgent/rvagent-learning/cloudbuild.yaml
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Cloud Build configuration for rvagent-learning
|
||||
# Deploys to Cloud Run in ruv-dev project
|
||||
|
||||
steps:
|
||||
# Build the Docker image
|
||||
- name: 'gcr.io/cloud-builders/docker'
|
||||
args:
|
||||
- 'build'
|
||||
- '-t'
|
||||
- 'gcr.io/$PROJECT_ID/rvagent-learning:$COMMIT_SHA'
|
||||
- '-t'
|
||||
- 'gcr.io/$PROJECT_ID/rvagent-learning:latest'
|
||||
- '-f'
|
||||
- 'crates/rvAgent/rvagent-learning/Dockerfile'
|
||||
- '.'
|
||||
|
||||
# Push to Container Registry
|
||||
- name: 'gcr.io/cloud-builders/docker'
|
||||
args:
|
||||
- 'push'
|
||||
- 'gcr.io/$PROJECT_ID/rvagent-learning:$COMMIT_SHA'
|
||||
|
||||
- name: 'gcr.io/cloud-builders/docker'
|
||||
args:
|
||||
- 'push'
|
||||
- 'gcr.io/$PROJECT_ID/rvagent-learning:latest'
|
||||
|
||||
# Deploy to Cloud Run
|
||||
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
|
||||
entrypoint: gcloud
|
||||
args:
|
||||
- 'run'
|
||||
- 'deploy'
|
||||
- 'rvagent-learning'
|
||||
- '--image'
|
||||
- 'gcr.io/$PROJECT_ID/rvagent-learning:$COMMIT_SHA'
|
||||
- '--region'
|
||||
- 'us-central1'
|
||||
- '--platform'
|
||||
- 'managed'
|
||||
- '--allow-unauthenticated'
|
||||
- '--memory'
|
||||
- '512Mi'
|
||||
- '--cpu'
|
||||
- '1'
|
||||
- '--timeout'
|
||||
- '300'
|
||||
- '--set-env-vars'
|
||||
- 'RUST_LOG=info,PI_RUVIO_URL=https://pi.ruv.io'
|
||||
- '--set-secrets'
|
||||
- 'GOOGLE_API_KEY=gemini-api-key:latest'
|
||||
|
||||
images:
|
||||
- 'gcr.io/$PROJECT_ID/rvagent-learning:$COMMIT_SHA'
|
||||
- 'gcr.io/$PROJECT_ID/rvagent-learning:latest'
|
||||
|
||||
options:
|
||||
logging: CLOUD_LOGGING_ONLY
|
||||
|
||||
timeout: '1200s'
|
||||
215
crates/rvAgent/rvagent-learning/src/bin/daily_cycle.rs
Normal file
215
crates/rvAgent/rvagent-learning/src/bin/daily_cycle.rs
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
//! Daily Learning Cycle CLI
|
||||
//!
|
||||
//! Run a single learning cycle or start the scheduler.
|
||||
//!
|
||||
//! Usage:
|
||||
//! cargo run -p rvagent-learning --bin daily_cycle -- [OPTIONS]
|
||||
//!
|
||||
//! Options:
|
||||
//! --once Run a single cycle and exit
|
||||
//! --status Show current state and exit
|
||||
//! --scan-dir Directory to scan (default: current directory)
|
||||
//! --dry-run Don't submit to π.ruv.io
|
||||
|
||||
use rvagent_learning::{
|
||||
DailyLearningLoop, SchedulerConfig,
|
||||
discovery::{CodebaseScanner, PatternAnalyzer, DiscoveryLog},
|
||||
goap::{GoapPlanner, LearningGoal, LearningWorldState},
|
||||
integration::PiRuvIoClient,
|
||||
};
|
||||
use std::env;
|
||||
use std::time::Instant;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// Initialize tracing
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::from_default_env()
|
||||
.add_directive("rvagent_learning=info".parse()?)
|
||||
)
|
||||
.init();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
let once = args.iter().any(|a| a == "--once");
|
||||
let status_only = args.iter().any(|a| a == "--status");
|
||||
let dry_run = args.iter().any(|a| a == "--dry-run");
|
||||
|
||||
let scan_dir = args.iter()
|
||||
.position(|a| a == "--scan-dir")
|
||||
.and_then(|i| args.get(i + 1))
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(".");
|
||||
|
||||
println!("╔══════════════════════════════════════════════════════════════╗");
|
||||
println!("║ RuVector Daily Learning Loop (ADR-115) ║");
|
||||
println!("╠══════════════════════════════════════════════════════════════╣");
|
||||
println!("║ GOAP-based discovery with Gemini 2.5 Flash reasoning ║");
|
||||
println!("║ Submits discoveries to π.ruv.io cloud brain ║");
|
||||
println!("╚══════════════════════════════════════════════════════════════╝");
|
||||
println!();
|
||||
|
||||
if status_only {
|
||||
show_status().await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if once {
|
||||
run_single_cycle(scan_dir, dry_run).await?;
|
||||
} else {
|
||||
run_scheduler(scan_dir).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_status() -> anyhow::Result<()> {
|
||||
println!("📊 System Status");
|
||||
println!("────────────────────────────────────────");
|
||||
|
||||
// Check π.ruv.io connection
|
||||
let pi_client = PiRuvIoClient::default_client();
|
||||
let connected = pi_client.check_connection().await;
|
||||
println!("π.ruv.io connection: {}", if connected { "✅ Connected" } else { "❌ Disconnected" });
|
||||
|
||||
// Check Gemini API key
|
||||
let gemini_available = env::var("GOOGLE_API_KEY").is_ok() || env::var("GEMINI_API_KEY").is_ok();
|
||||
println!("Gemini API key: {}", if gemini_available { "✅ Available" } else { "⚠️ Not set (set GOOGLE_API_KEY)" });
|
||||
|
||||
// Show current state
|
||||
let state = LearningWorldState::default();
|
||||
println!();
|
||||
println!("📈 Default State");
|
||||
println!("────────────────────────────────────────");
|
||||
println!("Patterns discovered: {}", state.patterns_discovered);
|
||||
println!("Pending submission: {}", state.patterns_pending_submission);
|
||||
println!("Memory utilization: {:.1}%", state.memory_utilization * 100.0);
|
||||
println!("Consolidation due: {}", state.consolidation_due);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_single_cycle(scan_dir: &str, dry_run: bool) -> anyhow::Result<()> {
|
||||
println!("🔄 Running single learning cycle...");
|
||||
println!(" Scan directory: {}", scan_dir);
|
||||
println!(" Dry run: {}", dry_run);
|
||||
println!();
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
// Phase 1: Scan codebase
|
||||
println!("📂 Phase 1: Scanning codebase...");
|
||||
let scanner = CodebaseScanner::new(scan_dir);
|
||||
let files = scanner.scan().await?;
|
||||
println!(" Found {} files to analyze", files.len());
|
||||
|
||||
// Phase 2: Analyze patterns
|
||||
println!("🔍 Phase 2: Analyzing patterns...");
|
||||
let analyzer = PatternAnalyzer::new();
|
||||
let file_contents: Vec<(String, String)> = files
|
||||
.into_iter()
|
||||
.map(|f| (f.path.to_string_lossy().to_string(), f.content))
|
||||
.collect();
|
||||
let discoveries = analyzer.analyze_files(&file_contents);
|
||||
println!(" Discovered {} patterns", discoveries.len());
|
||||
|
||||
// Show discoveries
|
||||
if !discoveries.is_empty() {
|
||||
println!();
|
||||
println!("📋 Discoveries");
|
||||
println!("────────────────────────────────────────");
|
||||
for (i, d) in discoveries.iter().take(10).enumerate() {
|
||||
println!("{}. [{}] {}", i + 1, format!("{:?}", d.category), d.title);
|
||||
println!(" Quality: {:.2} | Files: {:?}", d.quality.composite, d.source_files);
|
||||
println!(" Method: {}", d.method_attribution());
|
||||
}
|
||||
if discoveries.len() > 10 {
|
||||
println!(" ... and {} more", discoveries.len() - 10);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: GOAP Planning
|
||||
println!();
|
||||
println!("🧠 Phase 3: GOAP Planning...");
|
||||
let planner = GoapPlanner::new();
|
||||
let mut state = LearningWorldState::default();
|
||||
state.patterns_discovered = discoveries.len();
|
||||
|
||||
let goal = LearningGoal::SubmitToCloudBrain { min_quality: 0.7 };
|
||||
let plan = planner.plan(&state, &goal)?;
|
||||
|
||||
println!(" Plan: {} actions, cost: {:.1}", plan.actions.len(), plan.estimated_cost);
|
||||
for action in &plan.actions {
|
||||
println!(" - {} (cost: {:.1})", action.action, action.cost);
|
||||
}
|
||||
|
||||
// Phase 4: Submit to π.ruv.io (if not dry run)
|
||||
if !dry_run && !discoveries.is_empty() {
|
||||
println!();
|
||||
println!("☁️ Phase 4: Submitting to π.ruv.io...");
|
||||
let pi_client = PiRuvIoClient::default_client();
|
||||
|
||||
if pi_client.check_connection().await {
|
||||
let high_quality: Vec<&DiscoveryLog> = discoveries
|
||||
.iter()
|
||||
.filter(|d| d.quality.composite >= 0.5)
|
||||
.collect();
|
||||
|
||||
println!(" {} high-quality discoveries to submit", high_quality.len());
|
||||
|
||||
for discovery in high_quality.iter().take(3) {
|
||||
match pi_client.submit(discovery).await {
|
||||
Ok(response) => {
|
||||
if response.success {
|
||||
println!(" ✅ Submitted: {} -> {}",
|
||||
discovery.title,
|
||||
response.memory_id.unwrap_or_default());
|
||||
} else {
|
||||
println!(" ❌ Rejected: {}", response.error.unwrap_or_default());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" ⚠️ Failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(" ⚠️ π.ruv.io not connected, skipping submission");
|
||||
}
|
||||
} else if dry_run {
|
||||
println!();
|
||||
println!("☁️ Phase 4: Skipped (dry run mode)");
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
println!();
|
||||
println!("════════════════════════════════════════");
|
||||
println!("✅ Cycle complete in {:.2}s", duration.as_secs_f64());
|
||||
println!(" Patterns found: {}", discoveries.len());
|
||||
println!("════════════════════════════════════════");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_scheduler(scan_dir: &str) -> anyhow::Result<()> {
|
||||
println!("🕐 Starting scheduled learning loop...");
|
||||
println!(" Press Ctrl+C to stop");
|
||||
println!();
|
||||
|
||||
let mut config = SchedulerConfig::default();
|
||||
config.scan.root_directory = scan_dir.to_string();
|
||||
|
||||
let mut learning_loop = DailyLearningLoop::new(config).await?;
|
||||
|
||||
// Run first cycle immediately
|
||||
println!("Running initial cycle...");
|
||||
let result = learning_loop.run_cycle().await?;
|
||||
println!("Initial cycle: {} discoveries, {} submitted",
|
||||
result.discoveries_found, result.discoveries_submitted);
|
||||
|
||||
// Start scheduled loop
|
||||
learning_loop.start().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue