Orchestration UI: editor med tre visninger, sanntids kompilering, testkjøring (oppgave 24.6)
Backend: - POST /intentions/compile_script — kompilerer script, returnerer diagnostikk + kompilert resultat - POST /intentions/test_orchestration — trigger manuell testkjøring via jobbkø - GET /query/orchestration_log — henter kjørehistorikk for en orkestrering - "orchestration" lagt til som gyldig trait for samlingsnoder Frontend: - OrchestrationTrait.svelte — BlockShell-panel med: - Tre tabber: Enkel (editor), Teknisk (kompilert CLI), Kompilert (JSON) - Sanntids kompilering med 500ms debounce og diagnostikk-visning - Trigger-velger (6 event-typer) og executor-velger (script/bot/dream) - "Test kjøring"-knapp (lagrer + oppretter testjobb i køen) - Kjørehistorikk-panel med steg-status, varighet, feilmeldinger - Responsiv: container queries + mobile viewport fallback - Registrert i collection-page (mobil + desktop), workspace/types.ts - API-funksjoner: compileScript, testOrchestration, fetchOrchestrationLog
This commit is contained in:
parent
984f5e2f75
commit
376bf7ee62
7 changed files with 1223 additions and 5 deletions
|
|
@ -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<CompileScriptResponse> {
|
||||
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<TestOrchestrationResponse> {
|
||||
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<OrchestrationLogResponse> {
|
||||
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,
|
||||
|
|
|
|||
951
frontend/src/lib/components/traits/OrchestrationTrait.svelte
Normal file
951
frontend/src/lib/components/traits/OrchestrationTrait.svelte
Normal file
|
|
@ -0,0 +1,951 @@
|
|||
<script lang="ts">
|
||||
import type { Node } from '$lib/realtime';
|
||||
import { edgeStore, nodeStore, nodeVisibility } from '$lib/realtime';
|
||||
import {
|
||||
updateNode,
|
||||
compileScript,
|
||||
testOrchestration,
|
||||
fetchOrchestrationLog,
|
||||
type CompileScriptResponse,
|
||||
type OrchestrationLogEntry
|
||||
} from '$lib/api';
|
||||
|
||||
interface Props {
|
||||
collection?: Node;
|
||||
config: Record<string, unknown>;
|
||||
userId?: string;
|
||||
accessToken?: string;
|
||||
}
|
||||
|
||||
let { collection, config, userId, accessToken }: Props = $props();
|
||||
|
||||
// =========================================================================
|
||||
// Orchestration nodes in this collection
|
||||
// =========================================================================
|
||||
|
||||
const orchestrationNodes = $derived.by(() => {
|
||||
const nodes: Node[] = [];
|
||||
if (!collection) return nodes;
|
||||
for (const edge of edgeStore.byTarget(collection.id)) {
|
||||
if (edge.edgeType !== 'belongs_to') continue;
|
||||
const node = nodeStore.get(edge.sourceId);
|
||||
if (!node || nodeVisibility(node, userId) === 'hidden') continue;
|
||||
if (node.nodeKind === 'orchestration') {
|
||||
nodes.push(node);
|
||||
}
|
||||
}
|
||||
nodes.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
|
||||
return nodes;
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// State
|
||||
// =========================================================================
|
||||
|
||||
/** Currently selected orchestration node ID */
|
||||
let selectedId: string | undefined = $state(undefined);
|
||||
|
||||
/** Auto-select first orchestration node */
|
||||
$effect(() => {
|
||||
if (!selectedId && orchestrationNodes.length > 0) {
|
||||
selectedId = orchestrationNodes[0].id;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedNode = $derived(selectedId ? nodeStore.get(selectedId) : undefined);
|
||||
|
||||
const selectedMetadata = $derived.by((): Record<string, unknown> => {
|
||||
if (!selectedNode?.metadata) return {};
|
||||
try {
|
||||
return JSON.parse(selectedNode.metadata) as Record<string, unknown>;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
// Editor state
|
||||
let scriptContent = $state('');
|
||||
let activeTab: 'enkel' | 'teknisk' | 'kompilert' = $state('enkel');
|
||||
let compileResult = $state<CompileScriptResponse | null>(null);
|
||||
let compiling = $state(false);
|
||||
let saving = $state(false);
|
||||
let testRunning = $state(false);
|
||||
let lastTestJobId: string | null = $state(null);
|
||||
|
||||
// Trigger state
|
||||
let triggerEvent = $state('manual');
|
||||
let triggerConditions = $state('');
|
||||
let executor = $state('script');
|
||||
|
||||
// History
|
||||
let logEntries: OrchestrationLogEntry[] = $state([]);
|
||||
let showHistory = $state(false);
|
||||
|
||||
// Load content when selected node changes
|
||||
$effect(() => {
|
||||
if (selectedNode) {
|
||||
scriptContent = selectedNode.content ?? '';
|
||||
const meta = selectedMetadata;
|
||||
const trigger = meta.trigger as Record<string, unknown> | undefined;
|
||||
triggerEvent = (trigger?.event as string) ?? 'manual';
|
||||
const conditions = trigger?.conditions;
|
||||
triggerConditions = conditions ? JSON.stringify(conditions, null, 2) : '';
|
||||
executor = (meta.executor as string) ?? 'script';
|
||||
}
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Compile (debounced)
|
||||
// =========================================================================
|
||||
|
||||
let compileTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
$effect(() => {
|
||||
// React to script changes
|
||||
const _content = scriptContent;
|
||||
clearTimeout(compileTimeout);
|
||||
if (!accessToken || !scriptContent.trim()) {
|
||||
compileResult = null;
|
||||
return;
|
||||
}
|
||||
compileTimeout = setTimeout(async () => {
|
||||
compiling = true;
|
||||
try {
|
||||
compileResult = await compileScript(accessToken!, scriptContent);
|
||||
} catch (err) {
|
||||
console.error('Compile failed:', err);
|
||||
} finally {
|
||||
compiling = false;
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Derived views
|
||||
// =========================================================================
|
||||
|
||||
const technicalView = $derived(compileResult?.compiled?.technical ?? '(ingen kompilert utdata)');
|
||||
|
||||
const compiledJson = $derived(
|
||||
compileResult?.compiled
|
||||
? JSON.stringify(compileResult.compiled, null, 2)
|
||||
: '(kompilering feilet eller ingen inndata)'
|
||||
);
|
||||
|
||||
const errorCount = $derived(
|
||||
compileResult?.diagnostics?.filter((d) => d.severity === 'Error').length ?? 0
|
||||
);
|
||||
|
||||
const hasErrors = $derived(errorCount > 0);
|
||||
|
||||
// =========================================================================
|
||||
// Actions
|
||||
// =========================================================================
|
||||
|
||||
async function save() {
|
||||
if (!accessToken || !selectedId) return;
|
||||
saving = true;
|
||||
try {
|
||||
let conditionsObj: Record<string, unknown> | undefined;
|
||||
if (triggerConditions.trim()) {
|
||||
try {
|
||||
conditionsObj = JSON.parse(triggerConditions);
|
||||
} catch {
|
||||
// Invalid JSON, skip conditions
|
||||
}
|
||||
}
|
||||
|
||||
const metadata: Record<string, unknown> = {
|
||||
...selectedMetadata,
|
||||
trigger: {
|
||||
event: triggerEvent,
|
||||
...(conditionsObj ? { conditions: conditionsObj } : {}),
|
||||
},
|
||||
executor,
|
||||
};
|
||||
|
||||
// Include compiled pipeline if compilation succeeded
|
||||
if (compileResult?.compiled) {
|
||||
metadata.pipeline = compileResult.compiled.steps;
|
||||
if (compileResult.compiled.global_fallback) {
|
||||
metadata.global_fallback = compileResult.compiled.global_fallback;
|
||||
}
|
||||
}
|
||||
|
||||
await updateNode(accessToken, {
|
||||
node_id: selectedId,
|
||||
content: scriptContent,
|
||||
metadata,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Save failed:', err);
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runTest() {
|
||||
if (!accessToken || !selectedId) return;
|
||||
// Save first
|
||||
await save();
|
||||
testRunning = true;
|
||||
try {
|
||||
const result = await testOrchestration(accessToken, selectedId);
|
||||
lastTestJobId = result.job_id;
|
||||
} catch (err) {
|
||||
console.error('Test run failed:', err);
|
||||
} finally {
|
||||
testRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHistory() {
|
||||
if (!accessToken || !selectedId) return;
|
||||
try {
|
||||
const result = await fetchOrchestrationLog(accessToken, selectedId, 30);
|
||||
logEntries = result.entries;
|
||||
} catch (err) {
|
||||
console.error('Failed to load history:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleHistory() {
|
||||
showHistory = !showHistory;
|
||||
if (showHistory) loadHistory();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Trigger event options
|
||||
// =========================================================================
|
||||
|
||||
const TRIGGER_EVENTS = [
|
||||
{ value: 'node.created', label: 'Node opprettet' },
|
||||
{ value: 'edge.created', label: 'Edge opprettet' },
|
||||
{ value: 'communication.ended', label: 'Samtale avsluttet' },
|
||||
{ value: 'node.published', label: 'Node publisert' },
|
||||
{ value: 'scheduled.due', label: 'Planlagt tidspunkt' },
|
||||
{ value: 'manual', label: 'Manuell' },
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// Helpers
|
||||
// =========================================================================
|
||||
|
||||
function formatTime(iso: string): string {
|
||||
try {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleString('nb-NO', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="orch-trait">
|
||||
{#if orchestrationNodes.length === 0}
|
||||
<div class="orch-empty">
|
||||
<span class="orch-empty-icon">⚡</span>
|
||||
<p>Ingen orkestreringer i denne samlingen.</p>
|
||||
<p class="orch-empty-hint">
|
||||
Opprett en orchestration-node for å automatisere arbeidsflyter.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Orchestration selector (if multiple) -->
|
||||
{#if orchestrationNodes.length > 1}
|
||||
<div class="orch-selector">
|
||||
<select
|
||||
bind:value={selectedId}
|
||||
class="orch-select"
|
||||
>
|
||||
{#each orchestrationNodes as node (node.id)}
|
||||
<option value={node.id}>{node.title ?? 'Uten tittel'}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="orch-selector">
|
||||
<span class="orch-title">{orchestrationNodes[0]?.title ?? 'Orkestrering'}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Trigger configuration -->
|
||||
<div class="orch-trigger">
|
||||
<div class="orch-trigger-row">
|
||||
<label class="orch-label" for="trigger-event">Trigger</label>
|
||||
<select
|
||||
id="trigger-event"
|
||||
bind:value={triggerEvent}
|
||||
class="orch-select orch-select-sm"
|
||||
>
|
||||
{#each TRIGGER_EVENTS as evt (evt.value)}
|
||||
<option value={evt.value}>{evt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<label class="orch-label" for="executor-select">Executor</label>
|
||||
<select
|
||||
id="executor-select"
|
||||
bind:value={executor}
|
||||
class="orch-select orch-select-sm"
|
||||
>
|
||||
<option value="script">Script</option>
|
||||
<option value="bot">Bot</option>
|
||||
<option value="dream">Dream</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab bar -->
|
||||
<div class="orch-tabs">
|
||||
<button
|
||||
class="orch-tab"
|
||||
class:orch-tab-active={activeTab === 'enkel'}
|
||||
onclick={() => { activeTab = 'enkel'; }}
|
||||
>
|
||||
Enkel
|
||||
{#if hasErrors}
|
||||
<span class="orch-tab-badge orch-tab-badge-error">{errorCount}</span>
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
class="orch-tab"
|
||||
class:orch-tab-active={activeTab === 'teknisk'}
|
||||
onclick={() => { activeTab = 'teknisk'; }}
|
||||
>
|
||||
Teknisk
|
||||
</button>
|
||||
<button
|
||||
class="orch-tab"
|
||||
class:orch-tab-active={activeTab === 'kompilert'}
|
||||
onclick={() => { activeTab = 'kompilert'; }}
|
||||
>
|
||||
Kompilert
|
||||
</button>
|
||||
|
||||
<div class="orch-tab-spacer"></div>
|
||||
|
||||
{#if compiling}
|
||||
<span class="orch-status orch-status-compiling">Kompilerer...</span>
|
||||
{:else if compileResult && !hasErrors}
|
||||
<span class="orch-status orch-status-ok">OK</span>
|
||||
{:else if hasErrors}
|
||||
<span class="orch-status orch-status-error">{errorCount} feil</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Tab content -->
|
||||
<div class="orch-editor-area">
|
||||
{#if activeTab === 'enkel'}
|
||||
<div class="orch-editor-wrapper">
|
||||
<textarea
|
||||
class="orch-editor"
|
||||
bind:value={scriptContent}
|
||||
placeholder={"1. transkriber lydfilen (stor modell)\n ved feil: transkriber lydfilen (medium modell)\n2. oppsummer samtalen\n\nved feil: opprett oppgave \"Pipeline feilet\" (bug)"}
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
|
||||
<!-- Inline diagnostics -->
|
||||
{#if compileResult?.diagnostics && compileResult.diagnostics.length > 0}
|
||||
<div class="orch-diagnostics">
|
||||
{#each compileResult.diagnostics as diag (diag.line + '-' + diag.severity)}
|
||||
<div
|
||||
class="orch-diag"
|
||||
class:orch-diag-ok={diag.severity === 'Ok'}
|
||||
class:orch-diag-error={diag.severity === 'Error'}
|
||||
>
|
||||
<span class="orch-diag-icon">
|
||||
{diag.severity === 'Ok' ? '\u2713' : '\u2717'}
|
||||
</span>
|
||||
<span class="orch-diag-line">L{diag.line}</span>
|
||||
{#if diag.severity === 'Ok' && diag.compiled_output}
|
||||
<span class="orch-diag-text">{diag.compiled_output}</span>
|
||||
{:else if diag.severity === 'Error'}
|
||||
<span class="orch-diag-text">{diag.message}</span>
|
||||
{#if diag.suggestion}
|
||||
<span class="orch-diag-suggestion">Mente du: "{diag.suggestion}"?</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if activeTab === 'teknisk'}
|
||||
<pre class="orch-readonly">{technicalView}</pre>
|
||||
{:else if activeTab === 'kompilert'}
|
||||
<pre class="orch-readonly orch-readonly-json">{compiledJson}</pre>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="orch-actions">
|
||||
<button
|
||||
class="orch-btn orch-btn-primary"
|
||||
onclick={save}
|
||||
disabled={saving || !accessToken}
|
||||
>
|
||||
{saving ? 'Lagrer...' : 'Lagre'}
|
||||
</button>
|
||||
<button
|
||||
class="orch-btn orch-btn-test"
|
||||
onclick={runTest}
|
||||
disabled={testRunning || hasErrors || !accessToken || !scriptContent.trim()}
|
||||
title={hasErrors ? 'Rett feilene først' : 'Kjør orkestreringen manuelt'}
|
||||
>
|
||||
{testRunning ? 'Kjører...' : 'Test kjøring'}
|
||||
</button>
|
||||
<button
|
||||
class="orch-btn orch-btn-secondary"
|
||||
onclick={toggleHistory}
|
||||
>
|
||||
{showHistory ? 'Skjul historikk' : 'Historikk'}
|
||||
</button>
|
||||
|
||||
{#if lastTestJobId}
|
||||
<span class="orch-test-info">Jobb: {lastTestJobId.slice(0, 8)}...</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- History panel -->
|
||||
{#if showHistory}
|
||||
<div class="orch-history">
|
||||
<div class="orch-history-header">
|
||||
<span>Kjørehistorikk</span>
|
||||
<button class="orch-btn-refresh" onclick={loadHistory}>Oppdater</button>
|
||||
</div>
|
||||
{#if logEntries.length === 0}
|
||||
<p class="orch-history-empty">Ingen kjøringer ennå.</p>
|
||||
{:else}
|
||||
<div class="orch-history-list">
|
||||
{#each logEntries as entry (entry.id)}
|
||||
<div
|
||||
class="orch-history-item"
|
||||
class:orch-history-ok={entry.status === 'ok'}
|
||||
class:orch-history-error={entry.status === 'error'}
|
||||
>
|
||||
<span class="orch-history-status">
|
||||
{entry.status === 'ok' ? '\u2713' : '\u2717'}
|
||||
</span>
|
||||
<span class="orch-history-step">
|
||||
{#if entry.is_fallback}FB{:else}#{entry.step_number}{/if}
|
||||
</span>
|
||||
<span class="orch-history-binary">{entry.tool_binary}</span>
|
||||
{#if entry.duration_ms != null}
|
||||
<span class="orch-history-duration">{entry.duration_ms}ms</span>
|
||||
{/if}
|
||||
<span class="orch-history-time">{formatTime(entry.created_at)}</span>
|
||||
{#if entry.error_msg}
|
||||
<div class="orch-history-error-msg">{entry.error_msg}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* ================================================================= */
|
||||
/* Root */
|
||||
/* ================================================================= */
|
||||
.orch-trait {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ================================================================= */
|
||||
/* Empty state */
|
||||
/* ================================================================= */
|
||||
.orch-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
color: #9ca3af;
|
||||
text-align: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.orch-empty-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.orch-empty-hint {
|
||||
font-size: 11px;
|
||||
color: #d1d5db;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ================================================================= */
|
||||
/* Selector */
|
||||
/* ================================================================= */
|
||||
.orch-selector {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.orch-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.orch-select {
|
||||
width: 100%;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
background: white;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.orch-select-sm {
|
||||
width: auto;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
/* ================================================================= */
|
||||
/* Trigger config */
|
||||
/* ================================================================= */
|
||||
.orch-trigger {
|
||||
padding: 6px 12px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.orch-trigger-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.orch-label {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ================================================================= */
|
||||
/* Tabs */
|
||||
/* ================================================================= */
|
||||
.orch-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
flex-shrink: 0;
|
||||
padding: 0 8px;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.orch-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: color 0.12s, border-color 0.12s;
|
||||
}
|
||||
|
||||
.orch-tab:hover {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.orch-tab-active {
|
||||
color: #4f46e5;
|
||||
border-bottom-color: #4f46e5;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.orch-tab-badge {
|
||||
font-size: 10px;
|
||||
border-radius: 9999px;
|
||||
padding: 0 5px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.orch-tab-badge-error {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.orch-tab-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.orch-status {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 9999px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.orch-status-compiling {
|
||||
color: #6b7280;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.orch-status-ok {
|
||||
color: #059669;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
|
||||
.orch-status-error {
|
||||
color: #dc2626;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
/* ================================================================= */
|
||||
/* Editor area */
|
||||
/* ================================================================= */
|
||||
.orch-editor-area {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.orch-editor-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.orch-editor {
|
||||
flex: 1;
|
||||
min-height: 120px;
|
||||
padding: 10px 12px;
|
||||
border: none;
|
||||
resize: none;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #1f2937;
|
||||
background: #fafbfc;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.orch-editor::placeholder {
|
||||
color: #c4c9d0;
|
||||
}
|
||||
|
||||
.orch-readonly {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #374151;
|
||||
background: #f9fafb;
|
||||
border: none;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.orch-readonly-json {
|
||||
font-size: 11px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
/* ================================================================= */
|
||||
/* Diagnostics */
|
||||
/* ================================================================= */
|
||||
.orch-diagnostics {
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding: 6px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
background: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.orch-diag {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.orch-diag-icon {
|
||||
flex-shrink: 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.orch-diag-ok .orch-diag-icon {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.orch-diag-error .orch-diag-icon {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.orch-diag-line {
|
||||
font-size: 10px;
|
||||
color: #9ca3af;
|
||||
font-variant-numeric: tabular-nums;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.orch-diag-text {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.orch-diag-error .orch-diag-text {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.orch-diag-suggestion {
|
||||
color: #7c3aed;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ================================================================= */
|
||||
/* Actions */
|
||||
/* ================================================================= */
|
||||
.orch-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.orch-btn {
|
||||
padding: 5px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s, opacity 0.1s;
|
||||
}
|
||||
|
||||
.orch-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.orch-btn-primary {
|
||||
background: #4f46e5;
|
||||
color: white;
|
||||
border-color: #4f46e5;
|
||||
}
|
||||
|
||||
.orch-btn-primary:hover:not(:disabled) {
|
||||
background: #4338ca;
|
||||
}
|
||||
|
||||
.orch-btn-test {
|
||||
background: #059669;
|
||||
color: white;
|
||||
border-color: #059669;
|
||||
}
|
||||
|
||||
.orch-btn-test:hover:not(:disabled) {
|
||||
background: #047857;
|
||||
}
|
||||
|
||||
.orch-btn-secondary {
|
||||
background: white;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.orch-btn-secondary:hover:not(:disabled) {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.orch-test-info {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* ================================================================= */
|
||||
/* History panel */
|
||||
/* ================================================================= */
|
||||
.orch-history {
|
||||
border-top: 1px solid #e5e7eb;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
background: #fafbfc;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.orch-history-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.orch-btn-refresh {
|
||||
padding: 2px 8px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.orch-btn-refresh:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.orch-history-empty {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.orch-history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.orch-history-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 12px;
|
||||
font-size: 11px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.orch-history-status {
|
||||
flex-shrink: 0;
|
||||
font-weight: 700;
|
||||
width: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.orch-history-ok .orch-history-status {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.orch-history-error .orch-history-status {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.orch-history-step {
|
||||
font-size: 10px;
|
||||
color: #6b7280;
|
||||
font-variant-numeric: tabular-nums;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.orch-history-binary {
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.orch-history-duration {
|
||||
font-size: 10px;
|
||||
color: #9ca3af;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.orch-history-time {
|
||||
font-size: 10px;
|
||||
color: #9ca3af;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.orch-history-error-msg {
|
||||
width: 100%;
|
||||
padding: 3px 0 3px 22px;
|
||||
font-size: 10px;
|
||||
color: #dc2626;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* ================================================================= */
|
||||
/* Responsive */
|
||||
/* ================================================================= */
|
||||
@container (max-width: 400px) {
|
||||
.orch-trigger-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.orch-select-sm {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.orch-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.orch-btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.orch-test-info {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.orch-trigger-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.orch-select-sm {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.orch-editor {
|
||||
min-height: 100px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -54,6 +54,7 @@ export const TRAIT_PANEL_INFO: Record<string, TraitPanelInfo> = {
|
|||
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 */
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
<StudioTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} />
|
||||
{:else if trait === 'mixer'}
|
||||
<MixerTrait collection={collectionNode} config={traits[trait]} {accessToken} />
|
||||
{:else if trait === 'orchestration'}
|
||||
<OrchestrationTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} />
|
||||
{/if}
|
||||
{:else}
|
||||
<GenericTrait name={trait} config={traits[trait]} />
|
||||
|
|
@ -409,6 +412,8 @@
|
|||
<StudioTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} />
|
||||
{:else if trait === 'mixer'}
|
||||
<MixerTrait collection={collectionNode} config={traits[trait]} {accessToken} />
|
||||
{:else if trait === 'orchestration'}
|
||||
<OrchestrationTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} />
|
||||
{/if}
|
||||
{:else}
|
||||
<GenericTrait name={trait} config={traits[trait]} />
|
||||
|
|
|
|||
|
|
@ -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<AppState>,
|
||||
_user: AuthUser,
|
||||
Json(req): Json<CompileScriptRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
|
||||
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<AppState>,
|
||||
user: AuthUser,
|
||||
Json(req): Json<TestOrchestrationRequest>,
|
||||
) -> Result<Json<TestOrchestrationResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
// Verifiser at noden er en orchestration-node
|
||||
let node = sqlx::query_as::<_, (String, Option<String>)>(
|
||||
"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<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OrchestrationLogEntry {
|
||||
pub id: String,
|
||||
pub job_id: Option<String>,
|
||||
pub step_number: i16,
|
||||
pub tool_binary: String,
|
||||
pub args: serde_json::Value,
|
||||
pub is_fallback: bool,
|
||||
pub status: String,
|
||||
pub exit_code: Option<i16>,
|
||||
pub error_msg: Option<String>,
|
||||
pub duration_ms: Option<i32>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OrchestrationLogResponse {
|
||||
pub entries: Vec<OrchestrationLogEntry>,
|
||||
}
|
||||
|
||||
/// Hent kjørehistorikk for en orkestrering.
|
||||
pub async fn orchestration_log(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthUser,
|
||||
axum::extract::Query(params): axum::extract::Query<OrchestrationLogParams>,
|
||||
) -> Result<Json<OrchestrationLogResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||
let limit = params.limit.unwrap_or(50).min(200);
|
||||
|
||||
let rows = sqlx::query_as::<_, (
|
||||
Uuid,
|
||||
Option<Uuid>,
|
||||
i16,
|
||||
String,
|
||||
serde_json::Value,
|
||||
bool,
|
||||
String,
|
||||
Option<i16>,
|
||||
Option<String>,
|
||||
Option<i32>,
|
||||
chrono::DateTime<chrono::Utc>,
|
||||
)>(
|
||||
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",
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
3
tasks.md
3
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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue