diff --git a/docs/concepts/orkestrering.md b/docs/concepts/orkestrering.md index 255aae2..eb3b8b6 100644 --- a/docs/concepts/orkestrering.md +++ b/docs/concepts/orkestrering.md @@ -573,7 +573,30 @@ Hvis script ikke er mulig: | Arbeidstavlen | Work items opprettes ved feil | | Responskvalitet | Intelligence/effort per orkestrering | -## 14. Bygger på +## 14. Seed-orkestreringer + +Fem standard-orkestreringer leveres som eksempler og utgangspunkt: + +| Orkestrering | Trigger | Hva den gjør | +|---|---|---| +| **Podcast: opptak → publisering** | `communication.ended` + podcast + audio | Transkriberer, oppsummerer, genererer RSS | +| **Publisering: artikkel → render → RSS** | `node.published` + publishing | Rendrer HTML, indeks, RSS | +| **AI-beriking: foreslå koblinger** | `node.created` + content | Foreslår emne-edges via AI | +| **Planlagt publisering** | `scheduled.due` + publishing | Rendrer og genererer RSS ved planlagt tid | +| **Podcast: oppsummering → lyd (TTS)** | `cascade` (fra podcast-pipeline) | Leser oppsummering som lyd | + +Podcast-pipeline → TTS er koblet med `triggers`-edge og demonstrerer +kaskade-mønsteret. Se `migrations/025_seed_orchestrations.sql`. + +### Flerords-verb + +Kompilatoren støtter flerords-verb (f.eks. "generer feed", "les opp", +"foreslå koblinger"). Parseren splitter alltid ved første mellomrom, +men kompilatoren prøver å sette verb + begynnelsen av objekt sammen +for å matche alias. "generer feed for samlingen" matcher alias +"generer feed" i synops-rss, med "for samlingen" som objekt. + +## 15. Bygger på - `docs/retninger/unix_filosofi.md` — CLI-verktøy som byggeklosser - `docs/retninger/interaksjonsmodell.md` — drag-and-drop for observes-edge - `docs/concepts/arbeidstavlen.md` — @bot, work items ved feil diff --git a/maskinrommet/src/script_compiler.rs b/maskinrommet/src/script_compiler.rs index 1c76a22..55b3e21 100644 --- a/maskinrommet/src/script_compiler.rs +++ b/maskinrommet/src/script_compiler.rs @@ -479,6 +479,43 @@ pub fn compile(parsed: &ParsedScript, registry: &ToolRegistry) -> CompileResult } } +/// Prøv å finne verktøy med flerords-verb. +/// +/// Parseren splitter "generer feed for samlingen" til verb="generer", object="feed for samlingen". +/// Hvis "generer" ikke matcher noe alias, prøv "generer feed" (verb + første ord av object), +/// og returner resten av object som effektivt objekt. +fn find_tool_with_multiword_verb<'a>( + step: &ParsedStep, + registry: &'a ToolRegistry, +) -> Result<(&'a ToolDef, String), Diagnostic> { + // 1. Prøv enkeltord-verb + if let Some(tool) = registry.find_by_verb(&step.verb) { + return Ok((tool, step.object.clone())); + } + + // 2. Prøv flerords-verb: "verb + N første ord av object" + if !step.object.is_empty() { + let object_words: Vec<&str> = step.object.split_whitespace().collect(); + for take in 1..=object_words.len().min(3) { + let candidate = format!("{} {}", step.verb, object_words[..take].join(" ")); + if let Some(tool) = registry.find_by_verb(&candidate) { + let remaining = object_words[take..].join(" "); + return Ok((tool, remaining)); + } + } + } + + // 3. Ingen match — gi feilmelding + Err(Diagnostic { + line: step.line_number, + severity: Severity::Error, + message: format!("\"{}\" matcher ingen verktøy", step.verb), + suggestion: registry.suggest(&step.verb), + raw_input: step.raw.clone(), + compiled_output: None, + }) +} + /// Kompiler ett steg. fn compile_step( step: &ParsedStep, @@ -489,25 +526,16 @@ fn compile_step( 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, - } - })?; + // Finn verktøy via verb — prøv først enkeltord, deretter flerords-verb + // (f.eks. "generer feed" der verb="generer" og object="feed for samlingen") + let (tool, effective_object) = find_tool_with_multiword_verb(step, registry)?; // 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()) { + if !effective_object.is_empty() { + if let Some(hint) = tool.args_hints.get(&effective_object.to_lowercase()) { // Hint kan inneholde variabel ({event.xxx}) eller flagg (--flag value) expand_hint(hint, &mut cli_args); } @@ -924,4 +952,44 @@ ved feil: opprett oppgave "Pipeline feilet" (bug) assert!(parsed.global_fallback.is_some()); assert_eq!(parsed.global_fallback.unwrap().verb, "opprett"); } + + #[test] + fn test_multiword_verb() { + // "oppdater rss-feed" er et flerords-alias. Parseren splitter til + // verb="oppdater", object="rss-feed". compile_step skal finne alias + // "oppdater rss-feed" via flerords-matching. + let registry = test_registry(); + let script = "1. oppdater rss-feed\n"; + let parsed = parse(script).unwrap(); + let result = compile(&parsed, ®istry); + // Bør kompilere OK — "oppdater" matcher direkte som alias + assert!(!result.has_errors()); + + // Test med alias som KUN finnes som flerord + let mut multi_registry = ToolRegistry { + tools: vec![ToolDef { + binary: "synops-rss".into(), + aliases: vec!["generer feed".into()], + description: "RSS".into(), + args_hints: HashMap::from([( + "for samlingen".into(), + "--collection-id {event.collection_id}".into(), + )]), + }], + }; + let script2 = "1. generer feed for samlingen\n"; + let parsed2 = parse(script2).unwrap(); + let result2 = compile(&parsed2, &multi_registry); + assert!( + !result2.has_errors(), + "Flerords-verb 'generer feed' bør matche: {:?}", + result2.diagnostics + ); + let compiled = result2.compiled.unwrap(); + assert_eq!(compiled.steps[0].binary, "synops-rss"); + assert_eq!( + compiled.steps[0].args, + vec!["--collection-id", "{event.collection_id}"] + ); + } } diff --git a/migrations/025_seed_orchestrations.sql b/migrations/025_seed_orchestrations.sql new file mode 100644 index 0000000..30f09f0 --- /dev/null +++ b/migrations/025_seed_orchestrations.sql @@ -0,0 +1,158 @@ +-- 025_seed_orchestrations.sql +-- Oppgave 24.9: Seed-orkestreringer for podcast-pipeline, publiseringsflyt, +-- og AI-beriking. Skrevet i menneskelig scriptspråk som kompileres av +-- script_compiler til tekniske CLI-kall. +-- +-- Disse fungerer som standard-orkestreringer og som eksempler for brukere +-- som vil lage egne. De erstatter hardkodet logikk i vaktmesteren. +-- +-- Ref: docs/concepts/orkestrering.md + +BEGIN; + +-- ============================================================================= +-- 1. Podcast-pipeline: opptak → transkribering → oppsummering → RSS +-- ============================================================================= +-- Erstatter hardkodet flyt: communication.ended → whisper_transcribe → summarize → rss +-- Trigger: communication.ended + samling med podcast-trait + lydfil +INSERT INTO nodes (id, node_kind, title, content, visibility, metadata, created_by) +VALUES ( + 'e0000000-0ac0-4000-b000-000000000001', + 'orchestration', + 'Podcast: opptak → publisering', + E'NÅR innspilling avsluttet\nHVIS samling har podcast\n\n1. transkriber lydfilen (stor modell)\n ved feil: transkriber lydfilen (medium modell)\n2. oppsummer samtalen\n3. generer feed for samlingen\n\nved feil: opprett oppgave "Podcast-pipeline feilet" (bug)', + 'discoverable', + '{ + "trigger": { + "event": "communication.ended", + "conditions": { + "has_trait": "podcast", + "has_media": "audio" + } + }, + "executor": "script", + "intelligence": 2, + "effort": 3, + "compiled": false + }'::jsonb, + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' +); + +-- ============================================================================= +-- 2. Publiseringsflyt: artikkel publisert → render → RSS +-- ============================================================================= +-- Erstatter hardkodet flyt: render_article → render_index → rss +-- Trigger: node.published (belongs_to med slot i publiseringssamling) +INSERT INTO nodes (id, node_kind, title, content, visibility, metadata, created_by) +VALUES ( + 'e0000000-0ac0-4000-b000-000000000002', + 'orchestration', + 'Publisering: artikkel → render → RSS', + E'NÅR artikkel publisert\nHVIS samling har publishing\n\n1. render noden (som artikkel)\n ved feil: opprett oppgave "Rendering feilet" (bug)\n2. render noden (som indeks, i samlingen)\n3. generer feed for samlingen\n\nved feil: opprett oppgave "Publisering feilet" (bug)', + 'discoverable', + '{ + "trigger": { + "event": "node.published", + "conditions": { + "has_trait": "publishing" + } + }, + "executor": "script", + "intelligence": 1, + "effort": 2, + "compiled": false + }'::jsonb, + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' +); + +-- ============================================================================= +-- 3. AI-beriking: nytt innhold → foreslå koblinger +-- ============================================================================= +-- Erstatter hardkodet flyt: node.created → suggest_edges +-- Trigger: node.created med node_kind content +INSERT INTO nodes (id, node_kind, title, content, visibility, metadata, created_by) +VALUES ( + 'e0000000-0ac0-4000-b000-000000000003', + 'orchestration', + 'AI-beriking: foreslå koblinger', + E'NÅR innhold opprettet\nHVIS node_kind er content\n\n1. foreslå koblinger for noden\n\nved feil: opprett oppgave "AI-beriking feilet" (bug)', + 'discoverable', + '{ + "trigger": { + "event": "node.created", + "conditions": { + "node_kind": "content" + } + }, + "executor": "script", + "intelligence": 2, + "effort": 1, + "compiled": false + }'::jsonb, + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' +); + +-- ============================================================================= +-- 4. Planlagt publisering: tidspunkt nådd → render → RSS → varsle +-- ============================================================================= +-- Trigger: scheduled.due — planlagt tidspunkt nådd +INSERT INTO nodes (id, node_kind, title, content, visibility, metadata, created_by) +VALUES ( + 'e0000000-0ac0-4000-b000-000000000004', + 'orchestration', + 'Planlagt publisering: timer → render → RSS', + E'NÅR planlagt tid nådd\nHVIS samling har publishing\n\n1. render noden (som artikkel)\n2. generer feed for samlingen\n\nved feil: opprett oppgave "Planlagt publisering feilet" (bug)', + 'discoverable', + '{ + "trigger": { + "event": "scheduled.due", + "conditions": { + "has_trait": "publishing" + } + }, + "executor": "script", + "intelligence": 1, + "effort": 2, + "compiled": false + }'::jsonb, + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' +); + +-- ============================================================================= +-- 5. Podcast-oppsummering til lyd (TTS) — kaskademål for podcast-pipeline +-- ============================================================================= +-- Designet som kaskademål: kobles via triggers-edge fra podcast-pipeline. +-- Demonstrerer kaskade-mønsteret fra oppgave 24.8. +INSERT INTO nodes (id, node_kind, title, content, visibility, metadata, created_by) +VALUES ( + 'e0000000-0ac0-4000-b000-000000000005', + 'orchestration', + 'Podcast: oppsummering → lyd (TTS)', + E'NÅR kaskade\n\n1. les opp teksten fra noden (på norsk)\n\nved feil: opprett oppgave "TTS-generering feilet" (bug)', + 'discoverable', + '{ + "trigger": { + "event": "cascade", + "conditions": {} + }, + "executor": "script", + "intelligence": 1, + "effort": 2, + "compiled": false + }'::jsonb, + 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11' +); + +-- ============================================================================= +-- Kaskade-edge: podcast-pipeline → TTS-oppsummering +-- ============================================================================= +-- Når podcast-pipeline fullføres, trigger TTS-genereringen automatisk. +INSERT INTO edges (source_id, target_id, edge_type, metadata) +VALUES ( + 'e0000000-0ac0-4000-b000-000000000001', -- Podcast-pipeline + 'e0000000-0ac0-4000-b000-000000000005', -- TTS-oppsummering + 'triggers', + '{"description": "Generer lydoppsummering etter podcast-prosessering"}'::jsonb +); + +COMMIT; diff --git a/tasks.md b/tasks.md index 772afe3..7bcdc8b 100644 --- a/tasks.md +++ b/tasks.md @@ -326,8 +326,7 @@ automatisk eskalering av intelligens ved feil, kompilering av velprøvde mønstr - [x] 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`. - [x] 24.7 AI-assistert oppretting: `synops-ai` med auto-generert systemprompt (fra cli_tool-noder) foreslår script fra fritekst-beskrivelse. Vaktmesteren validerer. Eventually-modus: lagre som work_item for Claude Code. - [x] 24.8 Kaskade: `triggers`-edge mellom orkestreringer. Output fra én trigger neste. Syklusdeteksjon for å unngå uendelige loops. -- [~] 24.9 Seed-orkestreringer: opprett standard-orkestreringer for podcast-pipeline, publiseringsflyt, og AI-beriking basert på eksisterende hardkodet logikk i vaktmesteren. Skrives i menneskelig scriptspråk. - > Påbegynt: 2026-03-18T18:00 +- [x] 24.9 Seed-orkestreringer: opprett standard-orkestreringer for podcast-pipeline, publiseringsflyt, og AI-beriking basert på eksisterende hardkodet logikk i vaktmesteren. Skrives i menneskelig scriptspråk. ## Fase 25: Web Clipper — `synops-clip`