diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index e771328..81d35d7 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1336,6 +1336,91 @@ export async function toggleMixerEffect( }); } +// ============================================================================= +// Orkestrering (oppgave 24.6) +// ============================================================================= + +export interface CompileScriptResponse { + diagnostics: Array<{ + line: number; + severity: 'Ok' | 'Error'; + message: string; + suggestion: string | null; + raw_input: string; + compiled_output: string | null; + }>; + compiled: { + steps: Array<{ + step_number: number; + binary: string; + args: string[]; + }>; + global_fallback: { + binary: string; + args: string[]; + } | null; + technical: string; + } | null; +} + +/** Kompiler et orkestreringsscript og få diagnostikk + kompilert resultat. */ +export function compileScript( + accessToken: string, + script: string +): Promise { + return post(accessToken, '/intentions/compile_script', { script }); +} + +export interface TestOrchestrationResponse { + job_id: string; +} + +/** Trigger en manuell testkjøring av en orkestrering. */ +export function testOrchestration( + accessToken: string, + orchestrationId: string +): Promise { + return post(accessToken, '/intentions/test_orchestration', { + orchestration_id: orchestrationId + }); +} + +export interface OrchestrationLogEntry { + id: string; + job_id: string | null; + step_number: number; + tool_binary: string; + args: unknown[]; + is_fallback: boolean; + status: string; + exit_code: number | null; + error_msg: string | null; + duration_ms: number | null; + created_at: string; +} + +export interface OrchestrationLogResponse { + entries: OrchestrationLogEntry[]; +} + +/** Hent kjørehistorikk for en orkestrering. */ +export async function fetchOrchestrationLog( + accessToken: string, + orchestrationId: string, + limit?: number +): Promise { + const sp = new URLSearchParams({ orchestration_id: orchestrationId }); + if (limit) sp.set('limit', String(limit)); + const res = await fetch(`${BASE_URL}/query/orchestration_log?${sp}`, { + headers: { Authorization: `Bearer ${accessToken}` } + }); + if (!res.ok) { + const body = await res.text(); + throw new Error(`orchestration_log failed (${res.status}): ${body}`); + } + return res.json(); +} + export async function setMixerRole( accessToken: string, roomId: string, diff --git a/frontend/src/lib/components/traits/OrchestrationTrait.svelte b/frontend/src/lib/components/traits/OrchestrationTrait.svelte new file mode 100644 index 0000000..ae8f5f6 --- /dev/null +++ b/frontend/src/lib/components/traits/OrchestrationTrait.svelte @@ -0,0 +1,951 @@ + + +
+ {#if orchestrationNodes.length === 0} +
+ +

Ingen orkestreringer i denne samlingen.

+

+ Opprett en orchestration-node for å automatisere arbeidsflyter. +

+
+ {:else} + + {#if orchestrationNodes.length > 1} +
+ +
+ {:else} +
+ {orchestrationNodes[0]?.title ?? 'Orkestrering'} +
+ {/if} + + +
+
+ + + + + +
+
+ + +
+ + + + +
+ + {#if compiling} + Kompilerer... + {:else if compileResult && !hasErrors} + OK + {:else if hasErrors} + {errorCount} feil + {/if} +
+ + +
+ {#if activeTab === 'enkel'} +
+ + + + {#if compileResult?.diagnostics && compileResult.diagnostics.length > 0} +
+ {#each compileResult.diagnostics as diag (diag.line + '-' + diag.severity)} +
+ + {diag.severity === 'Ok' ? '\u2713' : '\u2717'} + + L{diag.line} + {#if diag.severity === 'Ok' && diag.compiled_output} + {diag.compiled_output} + {:else if diag.severity === 'Error'} + {diag.message} + {#if diag.suggestion} + Mente du: "{diag.suggestion}"? + {/if} + {/if} +
+ {/each} +
+ {/if} +
+ {:else if activeTab === 'teknisk'} +
{technicalView}
+ {:else if activeTab === 'kompilert'} +
{compiledJson}
+ {/if} +
+ + +
+ + + + + {#if lastTestJobId} + Jobb: {lastTestJobId.slice(0, 8)}... + {/if} +
+ + + {#if showHistory} +
+
+ Kjørehistorikk + +
+ {#if logEntries.length === 0} +

Ingen kjøringer ennå.

+ {:else} +
+ {#each logEntries as entry (entry.id)} +
+ + {entry.status === 'ok' ? '\u2713' : '\u2717'} + + + {#if entry.is_fallback}FB{:else}#{entry.step_number}{/if} + + {entry.tool_binary} + {#if entry.duration_ms != null} + {entry.duration_ms}ms + {/if} + {formatTime(entry.created_at)} + {#if entry.error_msg} +
{entry.error_msg}
+ {/if} +
+ {/each} +
+ {/if} +
+ {/if} + {/if} +
+ + diff --git a/frontend/src/lib/workspace/types.ts b/frontend/src/lib/workspace/types.ts index e1d5143..46fa31d 100644 --- a/frontend/src/lib/workspace/types.ts +++ b/frontend/src/lib/workspace/types.ts @@ -54,6 +54,7 @@ export const TRAIT_PANEL_INFO: Record = { transcription: { title: 'Transkripsjon', icon: '📄', defaultWidth: 500, defaultHeight: 450 }, studio: { title: 'Studio', icon: '🎛️', defaultWidth: 550, defaultHeight: 450 }, mixer: { title: 'Mikser', icon: '🎚️', defaultWidth: 450, defaultHeight: 400 }, + orchestration: { title: 'Orkestrering', icon: '⚡', defaultWidth: 550, defaultHeight: 500 }, }; /** Default info for unknown traits */ diff --git a/frontend/src/routes/collection/[id]/+page.svelte b/frontend/src/routes/collection/[id]/+page.svelte index 6f3f460..0cbfcbb 100644 --- a/frontend/src/routes/collection/[id]/+page.svelte +++ b/frontend/src/routes/collection/[id]/+page.svelte @@ -34,6 +34,7 @@ import TranscriptionTrait from '$lib/components/traits/TranscriptionTrait.svelte'; import StudioTrait from '$lib/components/traits/StudioTrait.svelte'; import MixerTrait from '$lib/components/traits/MixerTrait.svelte'; + import OrchestrationTrait from '$lib/components/traits/OrchestrationTrait.svelte'; import GenericTrait from '$lib/components/traits/GenericTrait.svelte'; import TraitAdmin from '$lib/components/traits/TraitAdmin.svelte'; import NodeUsage from '$lib/components/NodeUsage.svelte'; @@ -70,7 +71,7 @@ /** Traits with dedicated components */ const knownTraits = new Set([ 'editor', 'chat', 'kanban', 'podcast', 'publishing', - 'rss', 'calendar', 'recording', 'transcription', 'studio', 'mixer' + 'rss', 'calendar', 'recording', 'transcription', 'studio', 'mixer', 'orchestration' ]); /** Count of child nodes */ @@ -354,6 +355,8 @@ {:else if trait === 'mixer'} + {:else if trait === 'orchestration'} + {/if} {:else} @@ -409,6 +412,8 @@ {:else if trait === 'mixer'} + {:else if trait === 'orchestration'} + {/if} {:else} diff --git a/maskinrommet/src/intentions.rs b/maskinrommet/src/intentions.rs index 5bea5ec..42a7543 100644 --- a/maskinrommet/src/intentions.rs +++ b/maskinrommet/src/intentions.rs @@ -44,7 +44,7 @@ const VALID_TRAITS: &[&str] = &[ // Kunnskap "knowledge_graph", "wiki", "glossary", "faq", "bibliography", // Automatisering & AI - "auto_tag", "auto_summarize", "digest", "bridge", "moderation", "ai_tool", + "auto_tag", "auto_summarize", "digest", "bridge", "moderation", "ai_tool", "orchestration", // Tilgang & fellesskap "membership", "roles", "invites", "paywall", "directory", // Ekstern integrasjon @@ -4326,6 +4326,179 @@ pub struct UpdatePriorityRuleRequest { pub block_during_livekit: bool, } +// ============================================================================= +// Orkestrering: kompilering og testkjøring (oppgave 24.6) +// ============================================================================= + +#[derive(Deserialize)] +pub struct CompileScriptRequest { + pub script: String, +} + +/// Kompiler et orkestreringsscript og returner diagnostikk + kompilert resultat. +/// Brukes av frontend-editoren for sanntids kompileringsfeedback. +pub async fn compile_script( + State(state): State, + _user: AuthUser, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + use crate::script_compiler; + + let result = script_compiler::compile_script(&state.db, &req.script) + .await + .map_err(|e| bad_request(&e))?; + + // Serialiser CompileResult til JSON + let json = serde_json::to_value(&result) + .map_err(|e| internal_error(&format!("Serialiseringsfeil: {e}")))?; + + Ok(Json(json)) +} + +#[derive(Deserialize)] +pub struct TestOrchestrationRequest { + pub orchestration_id: Uuid, +} + +#[derive(Serialize)] +pub struct TestOrchestrationResponse { + pub job_id: String, +} + +/// Kjør en orkestrering manuelt (testkjøring). +/// Oppretter en "orchestrate"-jobb med trigger_event = "manual". +pub async fn test_orchestration( + State(state): State, + user: AuthUser, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + // Verifiser at noden er en orchestration-node + let node = sqlx::query_as::<_, (String, Option)>( + "SELECT node_kind, content FROM nodes WHERE id = $1", + ) + .bind(req.orchestration_id) + .fetch_optional(&state.db) + .await + .map_err(|e| internal_error(&format!("DB-feil: {e}")))? + .ok_or_else(|| bad_request("Orkestreringsnode ikke funnet"))?; + + if node.0 != "orchestration" { + return Err(bad_request("Noden er ikke en orchestration-node")); + } + + // Opprett en jobb i køen + let job_id = sqlx::query_scalar::<_, Uuid>( + r#" + INSERT INTO job_queue (job_type, collection_node_id, payload, status, priority) + VALUES ('orchestrate', NULL, $1, 'pending', 5) + RETURNING id + "#, + ) + .bind(serde_json::json!({ + "orchestration_id": req.orchestration_id.to_string(), + "trigger_event": "manual", + "trigger_context": {}, + "test_run": true, + "initiated_by": user.node_id.to_string(), + })) + .fetch_one(&state.db) + .await + .map_err(|e| internal_error(&format!("Kunne ikke opprette testjobb: {e}")))?; + + tracing::info!( + orchestration_id = %req.orchestration_id, + job_id = %job_id, + user = %user.node_id, + "Manuell testkjøring av orkestrering startet" + ); + + Ok(Json(TestOrchestrationResponse { + job_id: job_id.to_string(), + })) +} + +#[derive(Deserialize)] +pub struct OrchestrationLogParams { + pub orchestration_id: Uuid, + pub limit: Option, +} + +#[derive(Serialize)] +pub struct OrchestrationLogEntry { + pub id: String, + pub job_id: Option, + pub step_number: i16, + pub tool_binary: String, + pub args: serde_json::Value, + pub is_fallback: bool, + pub status: String, + pub exit_code: Option, + pub error_msg: Option, + pub duration_ms: Option, + pub created_at: String, +} + +#[derive(Serialize)] +pub struct OrchestrationLogResponse { + pub entries: Vec, +} + +/// Hent kjørehistorikk for en orkestrering. +pub async fn orchestration_log( + State(state): State, + _user: AuthUser, + axum::extract::Query(params): axum::extract::Query, +) -> Result, (StatusCode, Json)> { + let limit = params.limit.unwrap_or(50).min(200); + + let rows = sqlx::query_as::<_, ( + Uuid, + Option, + i16, + String, + serde_json::Value, + bool, + String, + Option, + Option, + Option, + chrono::DateTime, + )>( + r#" + SELECT id, job_id, step_number, tool_binary, args, is_fallback, + status, exit_code, error_msg, duration_ms, created_at + FROM orchestration_log + WHERE orchestration_id = $1 + ORDER BY created_at DESC + LIMIT $2 + "#, + ) + .bind(params.orchestration_id) + .bind(limit) + .fetch_all(&state.db) + .await + .map_err(|e| internal_error(&format!("DB-feil: {e}")))?; + + let entries = rows + .into_iter() + .map(|r| OrchestrationLogEntry { + id: r.0.to_string(), + job_id: r.1.map(|u| u.to_string()), + step_number: r.2, + tool_binary: r.3, + args: r.4, + is_fallback: r.5, + status: r.6, + exit_code: r.7, + error_msg: r.8, + duration_ms: r.9, + created_at: r.10.to_rfc3339(), + }) + .collect(); + + Ok(Json(OrchestrationLogResponse { entries })) +} + // ============================================================================= // Tester // ============================================================================= @@ -4464,7 +4637,7 @@ mod tests { "chat", "forum", "comments", "guest_input", "announcements", "polls", "qa", "kanban", "calendar", "timeline", "table", "gallery", "bookmarks", "tags", "knowledge_graph", "wiki", "glossary", "faq", "bibliography", - "auto_tag", "auto_summarize", "digest", "bridge", "moderation", + "auto_tag", "auto_summarize", "digest", "bridge", "moderation", "orchestration", "membership", "roles", "invites", "paywall", "directory", "webhook", "import", "export", "ical_sync", ]; diff --git a/maskinrommet/src/main.rs b/maskinrommet/src/main.rs index 681a11f..536826f 100644 --- a/maskinrommet/src/main.rs +++ b/maskinrommet/src/main.rs @@ -258,6 +258,10 @@ async fn main() { .route("/custom-domain/sok", get(custom_domain::serve_custom_domain_search)) .route("/custom-domain/om", get(custom_domain::serve_custom_domain_about)) .route("/custom-domain/{article_id}", get(custom_domain::serve_custom_domain_article)) + // Orkestrering UI (oppgave 24.6) + .route("/intentions/compile_script", post(intentions::compile_script)) + .route("/intentions/test_orchestration", post(intentions::test_orchestration)) + .route("/query/orchestration_log", get(intentions::orchestration_log)) // Mixer-kanaler .route("/intentions/create_mixer_channel", post(mixer::create_mixer_channel)) .route("/intentions/set_gain", post(mixer::set_gain)) diff --git a/tasks.md b/tasks.md index e233d30..989532f 100644 --- a/tasks.md +++ b/tasks.md @@ -321,8 +321,7 @@ automatisk eskalering av intelligens ved feil, kompilering av velprøvde mønstr - [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. - [x] 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. - [x] 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`. - > Påbegynt: 2026-03-18T17:20 +- [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`. - [ ] 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. - [ ] 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.