Script-kompilator for orkestreringer (oppgave 24.3)
Parser menneskelig scriptspråk og kompilerer til tekniske CLI-kall.
"transkriber lydfilen (stor modell)" → "synops-transcribe --cas-hash {event.cas_hash} --model large"
Kompilatoren:
- Parser nummererte steg, ved_feil-fallbacks, og globale feilhåndterere
- Matcher verb mot cli_tool-noders aliases (case-insensitive)
- Mapper argumenter i parentes via args_hints
- Validerer variabelreferanser ({event.*}) mot kjent liste
- Fuzzy-matching med Levenshtein-avstand for forslag ved feil
- Rust-stil kompileringsrapport med ✓/✗ per linje
Integrert i jobbkøen: orchestrate-jobb kompilerer scriptet og
lagrer pipeline i metadata. Utførelse kommer i oppgave 24.5.
12 unit-tester dekker parser, kompilator, feilhåndtering og fuzzy-matching.
This commit is contained in:
parent
021ee46023
commit
cc23e26802
4 changed files with 1057 additions and 17 deletions
|
|
@ -24,6 +24,7 @@ use crate::maintenance::MaintenanceState;
|
||||||
use crate::pg_writes;
|
use crate::pg_writes;
|
||||||
use crate::publishing::IndexCache;
|
use crate::publishing::IndexCache;
|
||||||
use crate::resources::{self, PriorityRules};
|
use crate::resources::{self, PriorityRules};
|
||||||
|
use crate::script_compiler;
|
||||||
use crate::summarize;
|
use crate::summarize;
|
||||||
use crate::transcribe;
|
use crate::transcribe;
|
||||||
use crate::tts;
|
use crate::tts;
|
||||||
|
|
@ -217,27 +218,139 @@ async fn dispatch(
|
||||||
"pg_delete_edge" => {
|
"pg_delete_edge" => {
|
||||||
pg_writes::handle_delete_edge(job, db, index_cache).await
|
pg_writes::handle_delete_edge(job, db, index_cache).await
|
||||||
}
|
}
|
||||||
// Orchestration: trigger-evaluering har lagt jobben i kø,
|
// Orchestration: trigger-evaluering har lagt jobben i kø.
|
||||||
// men utførelsen implementeres i oppgave 24.3.
|
// Kompilatoren parser scriptet og validerer det.
|
||||||
// Foreløpig logger vi og returnerer OK.
|
// Utførelse av kompilert script kommer i oppgave 24.5.
|
||||||
"orchestrate" => {
|
"orchestrate" => {
|
||||||
let orch_id = job.payload.get("orchestration_id")
|
handle_orchestrate(job, db).await
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("ukjent");
|
|
||||||
tracing::info!(
|
|
||||||
orchestration_id = %orch_id,
|
|
||||||
"Orchestrate-jobb mottatt (utførelse kommer i oppgave 24.3)"
|
|
||||||
);
|
|
||||||
Ok(serde_json::json!({
|
|
||||||
"status": "pending_implementation",
|
|
||||||
"orchestration_id": orch_id,
|
|
||||||
"message": "Orchestration execution not yet implemented (task 24.3)"
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
other => Err(format!("Ukjent jobbtype: {other}")),
|
other => Err(format!("Ukjent jobbtype: {other}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for `orchestrate`-jobb — kompilerer orchestration-script.
|
||||||
|
///
|
||||||
|
/// Henter orchestration-nodens `content` (menneskelig script),
|
||||||
|
/// kompilerer det via script_compiler, og lagrer resultatet
|
||||||
|
/// i nodens `metadata.pipeline`.
|
||||||
|
///
|
||||||
|
/// Utførelse av kompilert pipeline kommer i oppgave 24.5.
|
||||||
|
async fn handle_orchestrate(
|
||||||
|
job: &JobRow,
|
||||||
|
db: &PgPool,
|
||||||
|
) -> Result<serde_json::Value, String> {
|
||||||
|
let orch_id: Uuid = job
|
||||||
|
.payload
|
||||||
|
.get("orchestration_id")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.ok_or("Mangler orchestration_id i payload")?;
|
||||||
|
|
||||||
|
tracing::info!(orchestration_id = %orch_id, "Kompilerer orchestration-script");
|
||||||
|
|
||||||
|
// Hent orchestration-nodens content og metadata
|
||||||
|
let row = sqlx::query_as::<_, (Option<String>, serde_json::Value)>(
|
||||||
|
"SELECT content, metadata FROM nodes WHERE id = $1 AND node_kind = 'orchestration'"
|
||||||
|
)
|
||||||
|
.bind(orch_id)
|
||||||
|
.fetch_optional(db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Feil ved henting av orchestration-node: {e}"))?
|
||||||
|
.ok_or_else(|| format!("Orchestration-node {orch_id} finnes ikke"))?;
|
||||||
|
|
||||||
|
let (content, _metadata) = row;
|
||||||
|
let script = content.ok_or("Orchestration-node mangler content (script)")?;
|
||||||
|
|
||||||
|
if script.trim().is_empty() {
|
||||||
|
return Err("Orchestration-script er tomt".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse scriptet
|
||||||
|
let parsed = script_compiler::parse(&script)
|
||||||
|
.map_err(|e| format!("Parse-feil: {e}"))?;
|
||||||
|
|
||||||
|
// Hent verktøyregister fra cli_tool-noder
|
||||||
|
let registry = script_compiler::load_tool_registry(db).await?;
|
||||||
|
|
||||||
|
// Kompiler
|
||||||
|
let result = script_compiler::compile(&parsed, ®istry);
|
||||||
|
|
||||||
|
// Logg rapport
|
||||||
|
let report = result.format_report();
|
||||||
|
tracing::info!(
|
||||||
|
orchestration_id = %orch_id,
|
||||||
|
errors = result.has_errors(),
|
||||||
|
"\n{report}"
|
||||||
|
);
|
||||||
|
|
||||||
|
if result.has_errors() {
|
||||||
|
// Lagre feilrapport i metadata for UI-visning
|
||||||
|
let diagnostics_json = serde_json::to_value(&result.diagnostics)
|
||||||
|
.map_err(|e| format!("Serialiseringsfeil: {e}"))?;
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
r#"UPDATE nodes
|
||||||
|
SET metadata = jsonb_set(
|
||||||
|
jsonb_set(metadata, '{compile_errors}', $2),
|
||||||
|
'{compiled}', 'false'
|
||||||
|
)
|
||||||
|
WHERE id = $1"#,
|
||||||
|
)
|
||||||
|
.bind(orch_id)
|
||||||
|
.bind(&diagnostics_json)
|
||||||
|
.execute(db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Feil ved lagring av kompileringsfeil: {e}"))?;
|
||||||
|
|
||||||
|
return Err(format!("Kompilering feilet:\n{report}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suksess — lagre kompilert pipeline i metadata
|
||||||
|
let compiled = result.compiled.as_ref().unwrap();
|
||||||
|
let pipeline_json = serde_json::to_value(&compiled.steps)
|
||||||
|
.map_err(|e| format!("Serialiseringsfeil: {e}"))?;
|
||||||
|
|
||||||
|
let global_fb_json = compiled
|
||||||
|
.global_fallback
|
||||||
|
.as_ref()
|
||||||
|
.map(|fb| serde_json::to_value(fb).unwrap_or_default())
|
||||||
|
.unwrap_or(serde_json::Value::Null);
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
r#"UPDATE nodes
|
||||||
|
SET metadata = jsonb_set(
|
||||||
|
jsonb_set(
|
||||||
|
jsonb_set(
|
||||||
|
metadata - 'compile_errors',
|
||||||
|
'{pipeline}', $2
|
||||||
|
),
|
||||||
|
'{compiled}', 'true'
|
||||||
|
),
|
||||||
|
'{global_fallback}', $3
|
||||||
|
)
|
||||||
|
WHERE id = $1"#,
|
||||||
|
)
|
||||||
|
.bind(orch_id)
|
||||||
|
.bind(&pipeline_json)
|
||||||
|
.bind(&global_fb_json)
|
||||||
|
.execute(db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Feil ved lagring av kompilert pipeline: {e}"))?;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
orchestration_id = %orch_id,
|
||||||
|
steps = compiled.steps.len(),
|
||||||
|
"Orchestration-script kompilert"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"status": "compiled",
|
||||||
|
"orchestration_id": orch_id.to_string(),
|
||||||
|
"steps": compiled.steps.len(),
|
||||||
|
"technical": compiled.technical,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
/// Synops-render binary path.
|
/// Synops-render binary path.
|
||||||
fn render_bin() -> String {
|
fn render_bin() -> String {
|
||||||
std::env::var("SYNOPS_RENDER_BIN")
|
std::env::var("SYNOPS_RENDER_BIN")
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ pub mod summarize;
|
||||||
pub mod ws;
|
pub mod ws;
|
||||||
pub mod mixer;
|
pub mod mixer;
|
||||||
pub mod orchestration_trigger;
|
pub mod orchestration_trigger;
|
||||||
|
pub mod script_compiler;
|
||||||
pub mod tiptap;
|
pub mod tiptap;
|
||||||
pub mod transcribe;
|
pub mod transcribe;
|
||||||
pub mod tts;
|
pub mod tts;
|
||||||
|
|
|
||||||
927
maskinrommet/src/script_compiler.rs
Normal file
927
maskinrommet/src/script_compiler.rs
Normal file
|
|
@ -0,0 +1,927 @@
|
||||||
|
//! Script-kompilator for orkestreringer.
|
||||||
|
//!
|
||||||
|
//! Parser menneskelig scriptspråk og kompilerer til tekniske CLI-kall.
|
||||||
|
//! Brukerens "transkriber lydfilen (stor modell)" blir til
|
||||||
|
//! "synops-transcribe --cas-hash {event.cas_hash} --model large".
|
||||||
|
//!
|
||||||
|
//! Kompilatoren er deterministisk — ingen AI, ingen nettverkskall.
|
||||||
|
//! Matchingen bruker `cli_tool`-noders `aliases` og `args_hints`.
|
||||||
|
//!
|
||||||
|
//! Ref: docs/concepts/orkestrering.md § 4 "To lag"
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Verktøyregister — representerer cli_tool-noder fra PG
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Et CLI-verktøy hentet fra en `cli_tool`-node i grafen.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ToolDef {
|
||||||
|
/// Binærnavn (f.eks. "synops-transcribe")
|
||||||
|
pub binary: String,
|
||||||
|
/// Norske alias-verb som brukeren kan skrive (f.eks. ["transkriber", "transkribering"])
|
||||||
|
pub aliases: Vec<String>,
|
||||||
|
/// Beskrivelse for feilmeldinger
|
||||||
|
pub description: String,
|
||||||
|
/// Mapping fra menneskelige argumenter til CLI-flagg/variabler
|
||||||
|
/// F.eks. "stor modell" → "--model large", "lydfilen" → "{event.cas_hash}"
|
||||||
|
pub args_hints: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Samling av tilgjengelige verktøy (bygget fra cli_tool-noder i PG).
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ToolRegistry {
|
||||||
|
pub tools: Vec<ToolDef>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToolRegistry {
|
||||||
|
/// Bygg et register fra cli_tool-noders metadata (JSONB).
|
||||||
|
pub fn from_metadata(rows: &[(serde_json::Value,)]) -> Self {
|
||||||
|
let tools = rows
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(meta,)| {
|
||||||
|
let binary = meta.get("binary")?.as_str()?.to_string();
|
||||||
|
let aliases: Vec<String> = meta
|
||||||
|
.get("aliases")?
|
||||||
|
.as_array()?
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||||
|
.collect();
|
||||||
|
let description = meta
|
||||||
|
.get("description")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let args_hints: HashMap<String, String> = meta
|
||||||
|
.get("args_hints")
|
||||||
|
.and_then(|v| v.as_object())
|
||||||
|
.map(|obj| {
|
||||||
|
obj.iter()
|
||||||
|
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
Some(ToolDef {
|
||||||
|
binary,
|
||||||
|
aliases,
|
||||||
|
description,
|
||||||
|
args_hints,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
ToolRegistry { tools }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finn verktøy basert på et verb (case-insensitive).
|
||||||
|
fn find_by_verb(&self, verb: &str) -> Option<&ToolDef> {
|
||||||
|
let lower = verb.to_lowercase();
|
||||||
|
self.tools.iter().find(|t| {
|
||||||
|
t.aliases.iter().any(|a| a.to_lowercase() == lower)
|
||||||
|
|| t.binary.to_lowercase() == lower
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alle tilgjengelige verb (for feilmeldinger).
|
||||||
|
fn all_aliases(&self) -> Vec<&str> {
|
||||||
|
self.tools
|
||||||
|
.iter()
|
||||||
|
.flat_map(|t| t.aliases.iter().map(|a| a.as_str()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fuzzy-match: finn nærmeste alias til et ukjent verb.
|
||||||
|
fn suggest(&self, verb: &str) -> Option<String> {
|
||||||
|
let lower = verb.to_lowercase();
|
||||||
|
let mut best: Option<(&str, usize)> = None;
|
||||||
|
for alias in self.all_aliases() {
|
||||||
|
let dist = levenshtein(&lower, &alias.to_lowercase());
|
||||||
|
if dist <= 3 {
|
||||||
|
if best.is_none() || dist < best.unwrap().1 {
|
||||||
|
best = Some((alias, dist));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best.map(|(a, _)| a.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// AST — parsert representasjon av menneskelig script
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Et parsert steg i scriptet.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ParsedStep {
|
||||||
|
/// Linjenummer i kildekoden (1-indeksert)
|
||||||
|
pub line_number: usize,
|
||||||
|
/// Stegnummer (1, 2, 3 ...)
|
||||||
|
pub step_number: usize,
|
||||||
|
/// Verbet brukeren skrev (f.eks. "transkriber")
|
||||||
|
pub verb: String,
|
||||||
|
/// Objektet etter verbet (f.eks. "lydfilen")
|
||||||
|
pub object: String,
|
||||||
|
/// Argumenter i parentes (f.eks. ["stor modell"])
|
||||||
|
pub args: Vec<String>,
|
||||||
|
/// Fallback ved feil (valgfri)
|
||||||
|
pub fallback: Option<Box<ParsedStep>>,
|
||||||
|
/// Råteksten slik brukeren skrev den
|
||||||
|
pub raw: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsert script med alle steg.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ParsedScript {
|
||||||
|
pub steps: Vec<ParsedStep>,
|
||||||
|
/// Global feilhåndtering (siste "ved feil:"-linje)
|
||||||
|
pub global_fallback: Option<ParsedStep>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Kompileringsresultat
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Et kompilert steg klart for utførelse.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CompiledStep {
|
||||||
|
pub step_number: usize,
|
||||||
|
pub binary: String,
|
||||||
|
pub args: Vec<String>,
|
||||||
|
/// Fallback-kommando ved feil
|
||||||
|
pub fallback: Option<Box<CompiledStep>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kompilert script — klart for vaktmesteren.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CompiledScript {
|
||||||
|
pub steps: Vec<CompiledStep>,
|
||||||
|
pub global_fallback: Option<CompiledStep>,
|
||||||
|
/// Teknisk lag — lesbar representasjon av kompilert script
|
||||||
|
pub technical: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// En diagnostisk melding fra kompilatoren.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct Diagnostic {
|
||||||
|
pub line: usize,
|
||||||
|
pub severity: Severity,
|
||||||
|
pub message: String,
|
||||||
|
pub suggestion: Option<String>,
|
||||||
|
pub raw_input: String,
|
||||||
|
pub compiled_output: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||||
|
pub enum Severity {
|
||||||
|
Ok,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resultat fra kompileringen.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct CompileResult {
|
||||||
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
|
pub compiled: Option<CompiledScript>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompileResult {
|
||||||
|
pub fn has_errors(&self) -> bool {
|
||||||
|
self.diagnostics.iter().any(|d| d.severity == Severity::Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formater diagnostikk som Rust-stil rapport.
|
||||||
|
pub fn format_report(&self) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
out.push_str("┌─ Kompilering ─────────────────────────────────────┐\n");
|
||||||
|
out.push_str("│ │\n");
|
||||||
|
|
||||||
|
for d in &self.diagnostics {
|
||||||
|
match d.severity {
|
||||||
|
Severity::Ok => {
|
||||||
|
out.push_str(&format!(
|
||||||
|
"│ ✓ Linje {}: {}\n",
|
||||||
|
d.line, d.raw_input
|
||||||
|
));
|
||||||
|
if let Some(ref compiled) = d.compiled_output {
|
||||||
|
out.push_str(&format!("│ → {compiled}\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Severity::Error => {
|
||||||
|
out.push_str(&format!(
|
||||||
|
"│ ✗ Linje {}: {}\n",
|
||||||
|
d.line, d.raw_input
|
||||||
|
));
|
||||||
|
out.push_str(&format!("│ Feil: {}\n", d.message));
|
||||||
|
if let Some(ref sug) = d.suggestion {
|
||||||
|
out.push_str(&format!("│ Mente du: \"{sug}\"?\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push_str("│ │\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
let errors = self.diagnostics.iter().filter(|d| d.severity == Severity::Error).count();
|
||||||
|
if errors == 0 {
|
||||||
|
out.push_str("│ Kompilering OK.\n");
|
||||||
|
} else {
|
||||||
|
out.push_str(&format!(
|
||||||
|
"│ {} feil. Rett opp og prøv igjen.\n",
|
||||||
|
errors
|
||||||
|
));
|
||||||
|
}
|
||||||
|
out.push_str("└───────────────────────────────────────────────────┘");
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Gyldige trigger-kontekstvariabler
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Gyldige variabelprefiks og felter fra TriggerContext.
|
||||||
|
const VALID_EVENT_VARS: &[&str] = &[
|
||||||
|
"event.node_id",
|
||||||
|
"event.node_kind",
|
||||||
|
"event.edge_type",
|
||||||
|
"event.source_id",
|
||||||
|
"event.target_id",
|
||||||
|
"event.op",
|
||||||
|
"event.cas_hash",
|
||||||
|
"event.communication_id",
|
||||||
|
"event.collection_id",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Sjekk om en variabelreferanse er gyldig.
|
||||||
|
fn is_valid_variable(var: &str) -> bool {
|
||||||
|
// Stripp { og }
|
||||||
|
let inner = var.trim_matches(|c| c == '{' || c == '}');
|
||||||
|
VALID_EVENT_VARS.contains(&inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Parser — menneskelig script → AST
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Parser det menneskelige scriptlaget til en AST.
|
||||||
|
///
|
||||||
|
/// Forventet format:
|
||||||
|
/// ```text
|
||||||
|
/// 1. transkriber lydfilen (stor modell)
|
||||||
|
/// ved feil: transkriber lydfilen (medium modell)
|
||||||
|
/// 2. oppsummer samtalen
|
||||||
|
///
|
||||||
|
/// ved feil: opprett oppgave "Pipeline feilet" (bug)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// NÅR/HVIS-linjer ignoreres (de er allerede i metadata.trigger).
|
||||||
|
pub fn parse(script: &str) -> Result<ParsedScript, String> {
|
||||||
|
let mut steps = Vec::new();
|
||||||
|
let mut global_fallback: Option<ParsedStep> = None;
|
||||||
|
let lines: Vec<&str> = script.lines().collect();
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
while i < lines.len() {
|
||||||
|
let line = lines[i].trim();
|
||||||
|
|
||||||
|
// Skip tomme linjer og NÅR/HVIS (allerede håndtert av trigger-metadata)
|
||||||
|
if line.is_empty()
|
||||||
|
|| line.starts_with("NÅR ")
|
||||||
|
|| line.starts_with("HVIS ")
|
||||||
|
{
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nummerert steg: "1. verb objekt (args)"
|
||||||
|
if let Some(step) = try_parse_numbered_step(line, i + 1) {
|
||||||
|
let mut step = step;
|
||||||
|
|
||||||
|
// Sjekk neste linje for innrykket "ved feil:" (steg-fallback).
|
||||||
|
// Kun innrykket fallback (starter med whitespace) er steg-fallback.
|
||||||
|
// Uinnrykket "ved feil:" er global fallback.
|
||||||
|
if i + 1 < lines.len() {
|
||||||
|
let next_raw = lines[i + 1];
|
||||||
|
let is_indented = next_raw.starts_with(' ') || next_raw.starts_with('\t');
|
||||||
|
if is_indented {
|
||||||
|
let next = next_raw.trim();
|
||||||
|
if let Some(fallback) = try_parse_fallback(next, i + 2) {
|
||||||
|
step.fallback = Some(Box::new(fallback));
|
||||||
|
i += 1; // Hopp over fallback-linjen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
steps.push(step);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global "ved feil:" (ikke innrykket under et steg)
|
||||||
|
if let Some(fb) = try_parse_fallback(line, i + 1) {
|
||||||
|
global_fallback = Some(fb);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ukjent linje — hopp over (kommentarer, blanke, etc.)
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if steps.is_empty() {
|
||||||
|
return Err("Scriptet inneholder ingen steg. Forventet format: \"1. verb objekt (args)\"".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ParsedScript {
|
||||||
|
steps,
|
||||||
|
global_fallback,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forsøk å parse en nummerert steg-linje.
|
||||||
|
/// Format: "<N>. <verb> <objekt> [(<arg1>, <arg2>)]"
|
||||||
|
fn try_parse_numbered_step(line: &str, line_number: usize) -> Option<ParsedStep> {
|
||||||
|
// Match "N. rest"
|
||||||
|
let dot_pos = line.find('.')?;
|
||||||
|
let num_str = &line[..dot_pos];
|
||||||
|
let step_number: usize = num_str.trim().parse().ok()?;
|
||||||
|
let rest = line[dot_pos + 1..].trim();
|
||||||
|
|
||||||
|
if rest.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (verb, object, args) = parse_step_content(rest);
|
||||||
|
|
||||||
|
Some(ParsedStep {
|
||||||
|
line_number,
|
||||||
|
step_number,
|
||||||
|
verb,
|
||||||
|
object,
|
||||||
|
args,
|
||||||
|
fallback: None,
|
||||||
|
raw: line.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse en "ved feil:"-linje.
|
||||||
|
fn try_parse_fallback(line: &str, line_number: usize) -> Option<ParsedStep> {
|
||||||
|
let lower = line.to_lowercase();
|
||||||
|
let rest = if lower.starts_with("ved feil:") {
|
||||||
|
line["ved feil:".len()..].trim()
|
||||||
|
} else if lower.starts_with("ved_feil:") {
|
||||||
|
line["ved_feil:".len()..].trim()
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
if rest.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (verb, object, args) = parse_step_content(rest);
|
||||||
|
|
||||||
|
Some(ParsedStep {
|
||||||
|
line_number,
|
||||||
|
step_number: 0,
|
||||||
|
verb,
|
||||||
|
object,
|
||||||
|
args,
|
||||||
|
fallback: None,
|
||||||
|
raw: line.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse innholdet av et steg: "verb objekt (arg1, arg2)"
|
||||||
|
/// Returnerer (verb, objekt, [args]).
|
||||||
|
fn parse_step_content(content: &str) -> (String, String, Vec<String>) {
|
||||||
|
// Ekstraher argumenter i parentes
|
||||||
|
let (main, args) = if let Some(paren_start) = content.find('(') {
|
||||||
|
let paren_end = content.rfind(')').unwrap_or(content.len());
|
||||||
|
let args_str = &content[paren_start + 1..paren_end];
|
||||||
|
let args: Vec<String> = args_str
|
||||||
|
.split(',')
|
||||||
|
.map(|a| a.trim().to_string())
|
||||||
|
.filter(|a| !a.is_empty())
|
||||||
|
.collect();
|
||||||
|
let main = content[..paren_start].trim();
|
||||||
|
(main, args)
|
||||||
|
} else {
|
||||||
|
(content.trim(), Vec::new())
|
||||||
|
};
|
||||||
|
|
||||||
|
// Splitt verb og objekt
|
||||||
|
let parts: Vec<&str> = main.splitn(2, ' ').collect();
|
||||||
|
let verb = parts[0].to_string();
|
||||||
|
let object = if parts.len() > 1 {
|
||||||
|
parts[1].trim().to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
(verb, object, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Kompilator — AST + ToolRegistry → CompiledScript
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Kompilerer et parsert script til tekniske CLI-kall.
|
||||||
|
///
|
||||||
|
/// Ser opp verktøy i registeret, mapper argumenter via `args_hints`,
|
||||||
|
/// og validerer variabelreferanser.
|
||||||
|
pub fn compile(parsed: &ParsedScript, registry: &ToolRegistry) -> CompileResult {
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
let mut compiled_steps = Vec::new();
|
||||||
|
|
||||||
|
for step in &parsed.steps {
|
||||||
|
match compile_step(step, registry) {
|
||||||
|
Ok((compiled, diag)) => {
|
||||||
|
diagnostics.push(diag);
|
||||||
|
compiled_steps.push(compiled);
|
||||||
|
}
|
||||||
|
Err(diag) => {
|
||||||
|
diagnostics.push(diag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kompiler global fallback
|
||||||
|
let global_fallback = parsed.global_fallback.as_ref().and_then(|fb| {
|
||||||
|
match compile_step(fb, registry) {
|
||||||
|
Ok((compiled, diag)) => {
|
||||||
|
diagnostics.push(diag);
|
||||||
|
Some(compiled)
|
||||||
|
}
|
||||||
|
Err(diag) => {
|
||||||
|
diagnostics.push(diag);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let has_errors = diagnostics.iter().any(|d| d.severity == Severity::Error);
|
||||||
|
|
||||||
|
let compiled = if has_errors {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let technical = format_technical(&compiled_steps, &global_fallback);
|
||||||
|
Some(CompiledScript {
|
||||||
|
steps: compiled_steps,
|
||||||
|
global_fallback,
|
||||||
|
technical,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
CompileResult {
|
||||||
|
diagnostics,
|
||||||
|
compiled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kompiler ett steg.
|
||||||
|
fn compile_step(
|
||||||
|
step: &ParsedStep,
|
||||||
|
registry: &ToolRegistry,
|
||||||
|
) -> Result<(CompiledStep, Diagnostic), Diagnostic> {
|
||||||
|
// Spesialtilfelle: "opprett oppgave" → work_item
|
||||||
|
if step.verb.to_lowercase() == "opprett" && step.object.to_lowercase().starts_with("oppgave") {
|
||||||
|
return compile_work_item(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finn verktøy via verb
|
||||||
|
let tool = registry.find_by_verb(&step.verb).ok_or_else(|| {
|
||||||
|
let suggestion = registry.suggest(&step.verb);
|
||||||
|
Diagnostic {
|
||||||
|
line: step.line_number,
|
||||||
|
severity: Severity::Error,
|
||||||
|
message: format!("\"{}\" matcher ingen verktøy", step.verb),
|
||||||
|
suggestion,
|
||||||
|
raw_input: step.raw.clone(),
|
||||||
|
compiled_output: None,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Bygg argumentliste
|
||||||
|
let mut cli_args = Vec::new();
|
||||||
|
|
||||||
|
// Map objektet via args_hints (f.eks. "lydfilen" → "--cas-hash {event.cas_hash}")
|
||||||
|
if !step.object.is_empty() {
|
||||||
|
if let Some(hint) = tool.args_hints.get(&step.object.to_lowercase()) {
|
||||||
|
// Hint kan inneholde variabel ({event.xxx}) eller flagg (--flag value)
|
||||||
|
expand_hint(hint, &mut cli_args);
|
||||||
|
}
|
||||||
|
// Objektet som ikke matcher args_hints ignoreres (det er kontekst for brukeren)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map argumenter i parentes
|
||||||
|
for arg in &step.args {
|
||||||
|
let lower = arg.to_lowercase();
|
||||||
|
if let Some(hint) = tool.args_hints.get(&lower) {
|
||||||
|
expand_hint(hint, &mut cli_args);
|
||||||
|
} else {
|
||||||
|
// Prøv å matche mot args_hints med fuzzy match
|
||||||
|
let suggestion = tool
|
||||||
|
.args_hints
|
||||||
|
.keys()
|
||||||
|
.find(|k| levenshtein(&lower, &k.to_lowercase()) <= 2)
|
||||||
|
.cloned();
|
||||||
|
return Err(Diagnostic {
|
||||||
|
line: step.line_number,
|
||||||
|
severity: Severity::Error,
|
||||||
|
message: format!(
|
||||||
|
"Ukjent argument \"{}\" for {}",
|
||||||
|
arg, tool.binary
|
||||||
|
),
|
||||||
|
suggestion,
|
||||||
|
raw_input: step.raw.clone(),
|
||||||
|
compiled_output: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valider at alle variabelreferanser i args er gyldige
|
||||||
|
for a in &cli_args {
|
||||||
|
if a.starts_with('{') && a.ends_with('}') && !is_valid_variable(a) {
|
||||||
|
return Err(Diagnostic {
|
||||||
|
line: step.line_number,
|
||||||
|
severity: Severity::Error,
|
||||||
|
message: format!("Ukjent variabel: {a}"),
|
||||||
|
suggestion: Some(format!(
|
||||||
|
"Gyldige variabler: {}",
|
||||||
|
VALID_EVENT_VARS.join(", ")
|
||||||
|
)),
|
||||||
|
raw_input: step.raw.clone(),
|
||||||
|
compiled_output: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kompiler fallback rekursivt
|
||||||
|
let fallback = step
|
||||||
|
.fallback
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|fb| match compile_step(fb, registry) {
|
||||||
|
Ok((compiled, _)) => Some(Box::new(compiled)),
|
||||||
|
Err(_) => None, // Fallback-feil er allerede rapportert
|
||||||
|
});
|
||||||
|
|
||||||
|
let compiled_output = format_step_cli(&tool.binary, &cli_args);
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
CompiledStep {
|
||||||
|
step_number: step.step_number,
|
||||||
|
binary: tool.binary.clone(),
|
||||||
|
args: cli_args,
|
||||||
|
fallback,
|
||||||
|
},
|
||||||
|
Diagnostic {
|
||||||
|
line: step.line_number,
|
||||||
|
severity: Severity::Ok,
|
||||||
|
message: String::new(),
|
||||||
|
suggestion: None,
|
||||||
|
raw_input: step.raw.clone(),
|
||||||
|
compiled_output: Some(compiled_output),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kompiler "opprett oppgave" → work_item.
|
||||||
|
fn compile_work_item(step: &ParsedStep) -> Result<(CompiledStep, Diagnostic), Diagnostic> {
|
||||||
|
// Ekstraher tittel: alt etter "oppgave" i objektet, eller i anførselstegn
|
||||||
|
let obj = &step.object;
|
||||||
|
let title = if let Some(start) = obj.find('"') {
|
||||||
|
let end = obj[start + 1..].find('"').map(|i| i + start + 1).unwrap_or(obj.len());
|
||||||
|
&obj[start + 1..end]
|
||||||
|
} else {
|
||||||
|
// Alt etter "oppgave "
|
||||||
|
obj.strip_prefix("oppgave").map(|s| s.trim()).unwrap_or(obj)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut args = vec![format!("\"{}\"", title)];
|
||||||
|
|
||||||
|
// Tags fra parentes-argumenter
|
||||||
|
for arg in &step.args {
|
||||||
|
args.push("--tag".to_string());
|
||||||
|
args.push(arg.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let compiled_output = format!("work_item {}", args.join(" "));
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
CompiledStep {
|
||||||
|
step_number: step.step_number,
|
||||||
|
binary: "work_item".to_string(),
|
||||||
|
args,
|
||||||
|
fallback: None,
|
||||||
|
},
|
||||||
|
Diagnostic {
|
||||||
|
line: step.line_number,
|
||||||
|
severity: Severity::Ok,
|
||||||
|
message: String::new(),
|
||||||
|
suggestion: None,
|
||||||
|
raw_input: step.raw.clone(),
|
||||||
|
compiled_output: Some(compiled_output),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ekspander en args_hint-verdi til CLI-argumenter.
|
||||||
|
/// F.eks. "--model large" → ["--model", "large"]
|
||||||
|
/// F.eks. "{event.cas_hash}" → ["{event.cas_hash}"]
|
||||||
|
/// F.eks. "--cas-hash {event.cas_hash}" → ["--cas-hash", "{event.cas_hash}"]
|
||||||
|
fn expand_hint(hint: &str, args: &mut Vec<String>) {
|
||||||
|
// Splitt på whitespace, men bevar {variabler} og "strenger" intakt
|
||||||
|
for token in hint.split_whitespace() {
|
||||||
|
args.push(token.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formater et steg som CLI-kommando.
|
||||||
|
fn format_step_cli(binary: &str, args: &[String]) -> String {
|
||||||
|
if args.is_empty() {
|
||||||
|
binary.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{} {}", binary, args.join(" "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formater kompilert script som teknisk lag-tekst.
|
||||||
|
fn format_technical(steps: &[CompiledStep], global_fallback: &Option<CompiledStep>) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
for step in steps {
|
||||||
|
out.push_str(&format!(
|
||||||
|
"{}. {}\n",
|
||||||
|
step.step_number,
|
||||||
|
format_step_cli(&step.binary, &step.args)
|
||||||
|
));
|
||||||
|
if let Some(ref fb) = step.fallback {
|
||||||
|
out.push_str(&format!(
|
||||||
|
" VED_FEIL: {}\n",
|
||||||
|
format_step_cli(&fb.binary, &fb.args)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(fb) = global_fallback {
|
||||||
|
out.push_str(&format!(
|
||||||
|
"\nVED_FEIL: {}\n",
|
||||||
|
format_step_cli(&fb.binary, &fb.args)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Hent verktøyregister fra PG
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Hent alle cli_tool-noder fra PG og bygg et ToolRegistry.
|
||||||
|
pub async fn load_tool_registry(db: &sqlx::PgPool) -> Result<ToolRegistry, String> {
|
||||||
|
let rows = sqlx::query_as::<_, (serde_json::Value,)>(
|
||||||
|
"SELECT metadata FROM nodes WHERE node_kind = 'cli_tool'",
|
||||||
|
)
|
||||||
|
.fetch_all(db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Feil ved henting av cli_tool-noder: {e}"))?;
|
||||||
|
|
||||||
|
Ok(ToolRegistry::from_metadata(&rows))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full kompileringspipeline: parse script → hent verktøy → kompiler.
|
||||||
|
///
|
||||||
|
/// Returnerer kompileringsresultat med diagnostikk og (ved suksess) kompilert script.
|
||||||
|
pub async fn compile_script(
|
||||||
|
db: &sqlx::PgPool,
|
||||||
|
script: &str,
|
||||||
|
) -> Result<CompileResult, String> {
|
||||||
|
let parsed = parse(script)?;
|
||||||
|
let registry = load_tool_registry(db).await?;
|
||||||
|
Ok(compile(&parsed, ®istry))
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Levenshtein-avstand (enkel implementasjon for fuzzy-matching)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
fn levenshtein(a: &str, b: &str) -> usize {
|
||||||
|
let a_chars: Vec<char> = a.chars().collect();
|
||||||
|
let b_chars: Vec<char> = b.chars().collect();
|
||||||
|
let m = a_chars.len();
|
||||||
|
let n = b_chars.len();
|
||||||
|
|
||||||
|
if m == 0 {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut prev: Vec<usize> = (0..=n).collect();
|
||||||
|
let mut curr = vec![0; n + 1];
|
||||||
|
|
||||||
|
for i in 1..=m {
|
||||||
|
curr[0] = i;
|
||||||
|
for j in 1..=n {
|
||||||
|
let cost = if a_chars[i - 1] == b_chars[j - 1] { 0 } else { 1 };
|
||||||
|
curr[j] = (prev[j] + 1)
|
||||||
|
.min(curr[j - 1] + 1)
|
||||||
|
.min(prev[j - 1] + cost);
|
||||||
|
}
|
||||||
|
std::mem::swap(&mut prev, &mut curr);
|
||||||
|
}
|
||||||
|
|
||||||
|
prev[n]
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tester
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
fn test_registry() -> ToolRegistry {
|
||||||
|
ToolRegistry {
|
||||||
|
tools: vec![
|
||||||
|
ToolDef {
|
||||||
|
binary: "synops-transcribe".into(),
|
||||||
|
aliases: vec!["transkriber".into(), "transkribering".into()],
|
||||||
|
description: "Whisper-transkribering".into(),
|
||||||
|
args_hints: HashMap::from([
|
||||||
|
("lydfilen".into(), "--cas-hash {event.cas_hash}".into()),
|
||||||
|
("stor modell".into(), "--model large".into()),
|
||||||
|
("medium modell".into(), "--model medium".into()),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
ToolDef {
|
||||||
|
binary: "synops-summarize".into(),
|
||||||
|
aliases: vec!["oppsummer".into()],
|
||||||
|
description: "AI-oppsummering".into(),
|
||||||
|
args_hints: HashMap::from([(
|
||||||
|
"samtalen".into(),
|
||||||
|
"--communication-id {event.communication_id}".into(),
|
||||||
|
)]),
|
||||||
|
},
|
||||||
|
ToolDef {
|
||||||
|
binary: "synops-rss".into(),
|
||||||
|
aliases: vec!["oppdater rss-feed".into(), "oppdater".into()],
|
||||||
|
description: "RSS-generering".into(),
|
||||||
|
args_hints: HashMap::from([(
|
||||||
|
"rss-feed".into(),
|
||||||
|
"--collection-id {event.collection_id}".into(),
|
||||||
|
)]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_basic_script() {
|
||||||
|
let script = r#"
|
||||||
|
NÅR innspilling avsluttet
|
||||||
|
HVIS samling har podcast
|
||||||
|
|
||||||
|
1. transkriber lydfilen (stor modell)
|
||||||
|
ved feil: transkriber lydfilen (medium modell)
|
||||||
|
2. oppsummer samtalen
|
||||||
|
3. oppdater rss-feed
|
||||||
|
|
||||||
|
ved feil: opprett oppgave "Pipeline feilet" (bug)
|
||||||
|
"#;
|
||||||
|
let parsed = parse(script).unwrap();
|
||||||
|
assert_eq!(parsed.steps.len(), 3);
|
||||||
|
assert_eq!(parsed.steps[0].verb, "transkriber");
|
||||||
|
assert_eq!(parsed.steps[0].object, "lydfilen");
|
||||||
|
assert_eq!(parsed.steps[0].args, vec!["stor modell"]);
|
||||||
|
assert!(parsed.steps[0].fallback.is_some());
|
||||||
|
assert_eq!(parsed.steps[1].verb, "oppsummer");
|
||||||
|
assert_eq!(parsed.steps[2].verb, "oppdater");
|
||||||
|
assert!(parsed.global_fallback.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compile_full_script() {
|
||||||
|
let script = r#"
|
||||||
|
1. transkriber lydfilen (stor modell)
|
||||||
|
ved feil: transkriber lydfilen (medium modell)
|
||||||
|
2. oppsummer samtalen
|
||||||
|
"#;
|
||||||
|
let parsed = parse(script).unwrap();
|
||||||
|
let registry = test_registry();
|
||||||
|
let result = compile(&parsed, ®istry);
|
||||||
|
|
||||||
|
assert!(!result.has_errors());
|
||||||
|
let compiled = result.compiled.unwrap();
|
||||||
|
assert_eq!(compiled.steps.len(), 2);
|
||||||
|
assert_eq!(compiled.steps[0].binary, "synops-transcribe");
|
||||||
|
assert_eq!(
|
||||||
|
compiled.steps[0].args,
|
||||||
|
vec!["--cas-hash", "{event.cas_hash}", "--model", "large"]
|
||||||
|
);
|
||||||
|
assert!(compiled.steps[0].fallback.is_some());
|
||||||
|
let fb = compiled.steps[0].fallback.as_ref().unwrap();
|
||||||
|
assert_eq!(fb.args, vec!["--cas-hash", "{event.cas_hash}", "--model", "medium"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compile_unknown_verb() {
|
||||||
|
let script = "1. send epost til deltakerne\n";
|
||||||
|
let parsed = parse(script).unwrap();
|
||||||
|
let registry = test_registry();
|
||||||
|
let result = compile(&parsed, ®istry);
|
||||||
|
|
||||||
|
assert!(result.has_errors());
|
||||||
|
assert_eq!(result.diagnostics[0].severity, Severity::Error);
|
||||||
|
assert!(result.diagnostics[0].message.contains("matcher ingen verktøy"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compile_unknown_arg() {
|
||||||
|
let script = "1. transkriber lydfilen (ekstra stor modell)\n";
|
||||||
|
let parsed = parse(script).unwrap();
|
||||||
|
let registry = test_registry();
|
||||||
|
let result = compile(&parsed, ®istry);
|
||||||
|
|
||||||
|
assert!(result.has_errors());
|
||||||
|
assert!(result.diagnostics[0].message.contains("Ukjent argument"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compile_work_item() {
|
||||||
|
let script = "1. opprett oppgave \"Pipeline feilet\" (bug)\n";
|
||||||
|
let parsed = parse(script).unwrap();
|
||||||
|
let registry = test_registry();
|
||||||
|
let result = compile(&parsed, ®istry);
|
||||||
|
|
||||||
|
assert!(!result.has_errors());
|
||||||
|
let compiled = result.compiled.unwrap();
|
||||||
|
assert_eq!(compiled.steps[0].binary, "work_item");
|
||||||
|
assert_eq!(compiled.steps[0].args, vec!["\"Pipeline feilet\"", "--tag", "bug"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compile_report_format() {
|
||||||
|
let script = "1. transkriber lydfilen (stor modell)\n2. send epost\n";
|
||||||
|
let parsed = parse(script).unwrap();
|
||||||
|
let registry = test_registry();
|
||||||
|
let result = compile(&parsed, ®istry);
|
||||||
|
|
||||||
|
let report = result.format_report();
|
||||||
|
assert!(report.contains("✓ Linje"));
|
||||||
|
assert!(report.contains("✗ Linje"));
|
||||||
|
assert!(report.contains("1 feil"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_empty_script() {
|
||||||
|
assert!(parse("").is_err());
|
||||||
|
assert!(parse("NÅR noe skjer\nHVIS noe\n").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_levenshtein_basic() {
|
||||||
|
assert_eq!(levenshtein("transkriber", "transkriber"), 0);
|
||||||
|
assert_eq!(levenshtein("transkiber", "transkriber"), 1);
|
||||||
|
assert_eq!(levenshtein("", "abc"), 3);
|
||||||
|
assert_eq!(levenshtein("abc", ""), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fuzzy_suggestion() {
|
||||||
|
let registry = test_registry();
|
||||||
|
// "transkiber" er nær "transkriber"
|
||||||
|
let suggestion = registry.suggest("transkiber");
|
||||||
|
assert_eq!(suggestion, Some("transkriber".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_metadata() {
|
||||||
|
let rows = vec![(json!({
|
||||||
|
"binary": "synops-transcribe",
|
||||||
|
"aliases": ["transkriber"],
|
||||||
|
"description": "Test",
|
||||||
|
"args_hints": {"lydfilen": "--cas-hash {event.cas_hash}"}
|
||||||
|
}),)];
|
||||||
|
let registry = ToolRegistry::from_metadata(&rows);
|
||||||
|
assert_eq!(registry.tools.len(), 1);
|
||||||
|
assert_eq!(registry.tools[0].binary, "synops-transcribe");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_valid_variables() {
|
||||||
|
assert!(is_valid_variable("{event.node_id}"));
|
||||||
|
assert!(is_valid_variable("{event.cas_hash}"));
|
||||||
|
assert!(!is_valid_variable("{event.nonexistent}"));
|
||||||
|
assert!(!is_valid_variable("{foo.bar}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_global_fallback_as_ved_feil() {
|
||||||
|
let script = "1. oppsummer samtalen\nved feil: opprett oppgave \"Feil\" (bug)\n";
|
||||||
|
let parsed = parse(script).unwrap();
|
||||||
|
assert!(parsed.global_fallback.is_some());
|
||||||
|
assert_eq!(parsed.global_fallback.unwrap().verb, "opprett");
|
||||||
|
}
|
||||||
|
}
|
||||||
3
tasks.md
3
tasks.md
|
|
@ -318,8 +318,7 @@ automatisk eskalering av intelligens ved feil, kompilering av velprøvde mønstr
|
||||||
|
|
||||||
- [x] 24.1 Orchestration node-type: legg til `orchestration` i maskinrommets node-validering. Metadata-skjema: `trigger` (event + conditions), `executor`, `intelligence`, `effort`, `compiled`, `pipeline`. Valider trigger-events mot kjent liste.
|
- [x] 24.1 Orchestration node-type: legg til `orchestration` i maskinrommets node-validering. Metadata-skjema: `trigger` (event + conditions), `executor`, `intelligence`, `effort`, `compiled`, `pipeline`. Valider trigger-events mot kjent liste.
|
||||||
- [x] 24.2 Trigger-evaluering i portvokteren: ved node/edge-events, sjekk om noen `orchestration`-noder matcher triggeren. Effektiv lookup (indeksert på `metadata.trigger.event`). Ingen LLM-kall for trigger-matching.
|
- [x] 24.2 Trigger-evaluering i portvokteren: ved node/edge-events, sjekk om noen `orchestration`-noder matcher triggeren. Effektiv lookup (indeksert på `metadata.trigger.event`). Ingen LLM-kall for trigger-matching.
|
||||||
- [~] 24.3 Script-kompilator: parser menneskelig scriptspråk ("transkriber lydfilen (stor modell)") og kompilerer til tekniske CLI-kall. Matcher verb mot `cli_tool`-noders `aliases`, argumenter mot `args_hints`, variabler fra trigger-kontekst. Rust-stil kompileringsfeil med forslag.
|
- [x] 24.3 Script-kompilator: parser menneskelig scriptspråk ("transkriber lydfilen (stor modell)") og kompilerer til tekniske CLI-kall. Matcher verb mot `cli_tool`-noders `aliases`, argumenter mot `args_hints`, variabler fra trigger-kontekst. Rust-stil kompileringsfeil med forslag.
|
||||||
> Påbegynt: 2026-03-18T16:55
|
|
||||||
- [ ] 24.4 cli_tool alias-metadata: utvid alle `cli_tool`-noder med `aliases` (norske verb) og `args_hints` (menneskelige argumenter → CLI-flagg). Seed for alle eksisterende verktøy.
|
- [ ] 24.4 cli_tool alias-metadata: utvid alle `cli_tool`-noder med `aliases` (norske verb) og `args_hints` (menneskelige argumenter → CLI-flagg). Seed for alle eksisterende verktøy.
|
||||||
- [ ] 24.5 Script-executor: vaktmesteren parser kompilert script og eksekverer steg sekvensielt via generisk dispatch. VED_FEIL-håndtering. Logger i `orchestration_log`.
|
- [ ] 24.5 Script-executor: vaktmesteren parser kompilert script og eksekverer steg sekvensielt via generisk dispatch. VED_FEIL-håndtering. Logger i `orchestration_log`.
|
||||||
- [ ] 24.6 Orchestration UI: editor med tre visninger (Enkel/Teknisk/Kompilert) som tabber. Sanntids kompileringsfeil. Trigger-velger, "Test kjøring"-knapp, kjørehistorikk. Ref: `docs/concepts/orkestrering.md`.
|
- [ ] 24.6 Orchestration UI: editor med tre visninger (Enkel/Teknisk/Kompilert) som tabber. Sanntids kompileringsfeil. Trigger-velger, "Test kjøring"-knapp, kjørehistorikk. Ref: `docs/concepts/orkestrering.md`.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue