Seed-orkestreringer og flerords-verbstøtte (oppgave 24.9)

Fem standard-orkestreringer opprettet som seed-data:
- Podcast-pipeline (transkriber → oppsummer → RSS)
- Publiseringsflyt (render → indeks → RSS)
- AI-beriking (foreslå koblinger ved nytt innhold)
- Planlagt publisering (render ved tidspunkt)
- Podcast TTS (kaskade fra pipeline → les opp oppsummering)

Podcast-pipeline → TTS demonstrerer kaskade via triggers-edge.

Script-kompilatoren utvidet med flerords-verbstøtte: aliaser som
"generer feed", "les opp", "foreslå koblinger" matcher nå korrekt
selv om parseren splitter ved første mellomrom. Prøver verb + N
første ord av objekt opptil 3 ord.
This commit is contained in:
vegard 2026-03-18 18:11:02 +00:00
parent 98654c4a84
commit 1f21f90f76
4 changed files with 265 additions and 17 deletions

View file

@ -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

View file

@ -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, &registry);
// 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}"]
);
}
}

View file

@ -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;

View file

@ -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`