Claude er nå en agent-node i grafen som kan delta i samtaler.
Når en bruker sender melding i en kommunikasjonsnode der Claude
er deltaker, enqueues en agent_respond-jobb som kaller claude CLI
direkte og skriver svaret tilbake til chatten.
Nye filer:
- migrations/007_agent_system.sql: agent_identities, agent_permissions, ai_usage_log
- maskinrommet/src/agent.rs: agent_respond job handler
- scripts/maskinrommet.service: systemd-tjeneste for native kjøring
- scripts/maskinrommet-env.sh: genererer env med Docker container-IPs
Endringer:
- intentions.rs: trigger agent_respond ved melding i agent-chat
- jobs.rs: dispatch agent_respond til agent-handler
- frontend chat: bot-badge (🤖) og amber-farge på agent-meldinger
- LiteLLM config: resonering-modellalias via OpenRouter
Maskinrommet kjører nå direkte på hosten (ikke i Docker) for å
ha tilgang til claude CLI. Caddy peker til host.docker.internal.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
190 lines
6.6 KiB
Rust
190 lines
6.6 KiB
Rust
pub mod agent;
|
|
mod auth;
|
|
pub mod cas;
|
|
mod intentions;
|
|
pub mod jobs;
|
|
mod queries;
|
|
mod serving;
|
|
mod stdb;
|
|
pub mod transcribe;
|
|
mod warmup;
|
|
|
|
use axum::{extract::State, http::StatusCode, routing::{get, post}, Json, Router};
|
|
use serde::Serialize;
|
|
use sqlx::postgres::PgPoolOptions;
|
|
use sqlx::PgPool;
|
|
use tower_http::trace::TraceLayer;
|
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
|
|
|
use auth::{AuthUser, JwksKeys};
|
|
use cas::CasStore;
|
|
use stdb::StdbClient;
|
|
|
|
#[derive(Clone)]
|
|
pub struct AppState {
|
|
pub db: PgPool,
|
|
pub jwks: JwksKeys,
|
|
pub stdb: StdbClient,
|
|
pub cas: CasStore,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct HealthResponse {
|
|
status: &'static str,
|
|
version: &'static str,
|
|
db: &'static str,
|
|
stdb: &'static str,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct MeResponse {
|
|
node_id: uuid::Uuid,
|
|
authentik_sub: String,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
tracing_subscriber::registry()
|
|
.with(
|
|
EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| "maskinrommet=debug,tower_http=debug".parse().unwrap()),
|
|
)
|
|
.with(tracing_subscriber::fmt::layer())
|
|
.init();
|
|
|
|
// Database
|
|
let database_url = std::env::var("DATABASE_URL")
|
|
.unwrap_or_else(|_| "postgres://sidelinja:sidelinja@localhost:5432/synops".to_string());
|
|
|
|
let db = PgPoolOptions::new()
|
|
.max_connections(10)
|
|
.connect(&database_url)
|
|
.await
|
|
.expect("Kunne ikke koble til PostgreSQL");
|
|
|
|
tracing::info!("Koblet til PostgreSQL");
|
|
|
|
// JWKS — hent nøkler fra Authentik ved oppstart
|
|
let issuer = std::env::var("AUTHENTIK_ISSUER")
|
|
.unwrap_or_else(|_| "https://auth.sidelinja.org/application/o/sidelinja/".to_string());
|
|
|
|
let client_id = std::env::var("AUTHENTIK_CLIENT_ID")
|
|
.expect("AUTHENTIK_CLIENT_ID må være satt");
|
|
|
|
let jwks = JwksKeys::fetch(&issuer, &client_id)
|
|
.await
|
|
.expect("Kunne ikke hente JWKS fra Authentik");
|
|
|
|
// SpacetimeDB-klient
|
|
let stdb_url = std::env::var("SPACETIMEDB_URL")
|
|
.unwrap_or_else(|_| "http://spacetimedb:3000".to_string());
|
|
let stdb_database = std::env::var("SPACETIMEDB_DATABASE")
|
|
.unwrap_or_else(|_| "synops".to_string());
|
|
|
|
// Hent token fra miljøvariabel, eller opprett ny identitet
|
|
let stdb_token = match std::env::var("SPACETIMEDB_TOKEN") {
|
|
Ok(token) if !token.is_empty() => {
|
|
tracing::info!("Bruker konfigurert STDB-token");
|
|
token
|
|
}
|
|
_ => {
|
|
tracing::info!("Ingen STDB-token konfigurert, oppretter ny identitet");
|
|
let (identity, token) = StdbClient::create_identity(&stdb_url)
|
|
.await
|
|
.expect("Kunne ikke opprette STDB-identitet");
|
|
tracing::info!("Opprettet STDB-identitet: {identity}");
|
|
token
|
|
}
|
|
};
|
|
|
|
let stdb = StdbClient::new(&stdb_url, &stdb_database, &stdb_token);
|
|
|
|
// Warmup: last hele grafen fra PG til SpacetimeDB
|
|
match warmup::run(&db, &stdb).await {
|
|
Ok(stats) => {
|
|
tracing::info!(
|
|
"Warmup fullført: {} noder, {} edges, {} access",
|
|
stats.nodes,
|
|
stats.edges,
|
|
stats.access
|
|
);
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Warmup feilet: {e}");
|
|
// Fortsett likevel — STDB kan være midlertidig utilgjengelig
|
|
}
|
|
}
|
|
|
|
// CAS — content-addressable store for binærfiler
|
|
let cas_root = std::env::var("CAS_ROOT")
|
|
.unwrap_or_else(|_| "/srv/synops/media/cas".to_string());
|
|
let cas = CasStore::new(&cas_root)
|
|
.await
|
|
.expect("Kunne ikke opprette CAS-katalog");
|
|
tracing::info!(root = %cas_root, "CAS initialisert");
|
|
|
|
// Start jobbkø-worker i bakgrunnen
|
|
jobs::start_worker(db.clone(), stdb.clone(), cas.clone());
|
|
|
|
let state = AppState { db, jwks, stdb, cas };
|
|
|
|
// Ruter: /health er offentlig, /me krever gyldig JWT
|
|
let app = Router::new()
|
|
.route("/health", get(health))
|
|
.route("/me", get(me))
|
|
.route("/intentions/create_node", post(intentions::create_node))
|
|
.route("/intentions/create_edge", post(intentions::create_edge))
|
|
.route("/intentions/update_node", post(intentions::update_node))
|
|
.route("/intentions/delete_node", post(intentions::delete_node))
|
|
.route("/intentions/create_communication", post(intentions::create_communication))
|
|
.route("/intentions/upload_media", post(intentions::upload_media))
|
|
.route("/cas/{hash}", get(serving::get_cas_file))
|
|
.route("/query/nodes", get(queries::query_nodes))
|
|
.route("/query/segments", get(queries::query_segments))
|
|
.route("/query/segments/srt", get(queries::export_srt))
|
|
.route("/intentions/create_alias", post(intentions::create_alias))
|
|
.route("/intentions/update_segment", post(intentions::update_segment))
|
|
.route("/intentions/retranscribe", post(intentions::retranscribe))
|
|
.route("/intentions/resolve_retranscription", post(intentions::resolve_retranscription))
|
|
.route("/query/aliases", get(queries::query_aliases))
|
|
.route("/query/transcription_versions", get(queries::query_transcription_versions))
|
|
.route("/query/segments_version", get(queries::query_segments_version))
|
|
.layer(TraceLayer::new_for_http())
|
|
.with_state(state);
|
|
|
|
let bind = std::env::var("BIND_ADDR").unwrap_or_else(|_| "0.0.0.0:3100".to_string());
|
|
let listener = tokio::net::TcpListener::bind(&bind).await.unwrap();
|
|
tracing::info!("Maskinrommet lytter på {bind}");
|
|
axum::serve(listener, app).await.unwrap();
|
|
}
|
|
|
|
/// Offentlig helsesjekk — verifiserer PG- og STDB-tilkobling.
|
|
async fn health(State(state): State<AppState>) -> Result<Json<HealthResponse>, StatusCode> {
|
|
sqlx::query("SELECT 1")
|
|
.execute(&state.db)
|
|
.await
|
|
.map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
|
|
|
|
// STDB helsesjekk: prøv å slette en ikke-eksisterende node.
|
|
// Kallet når STDB og returnerer ok (noop), men feiler ved nettverksfeil.
|
|
let stdb_status = match state.stdb.delete_node("__healthcheck_nonexistent__").await {
|
|
Ok(()) => "connected",
|
|
Err(_) => "unavailable",
|
|
};
|
|
|
|
Ok(Json(HealthResponse {
|
|
status: "ok",
|
|
version: env!("CARGO_PKG_VERSION"),
|
|
db: "connected",
|
|
stdb: stdb_status,
|
|
}))
|
|
}
|
|
|
|
/// Beskyttet endepunkt — returnerer autentisert brukers node_id.
|
|
/// Brukes for å verifisere at auth-middleware fungerer.
|
|
async fn me(user: AuthUser) -> Json<MeResponse> {
|
|
Json(MeResponse {
|
|
node_id: user.node_id,
|
|
authentik_sub: user.authentik_sub,
|
|
})
|
|
}
|