Ricerca dei percorsi, costruzione del piano ed esecuzione per trasformazioni in più passi.
Panoramica
Il modulo planner individua automaticamente i percorsi di trasformazione. Dati un tipo di media sorgente e un tipo di media di destinazione, trova una sequenza di cap che trasforma i dati senza che l’utente debba specificare i passi intermedi.
Questo è diverso dall’orchestratore (vedi Orchestratore), che prende una descrizione esplicita del DAG. Il planner costruisce il DAG esplorando il grafo delle capacità.
Sorgente: capdag-rs/src/planner/.
LiveCapFab
graph LR
PDF["media:document;ext=pdf"] -->|extract_metadata| REC["media:record"]
PDF -->|generate_thumbnail| IMG["media:ext=png;image"]
PDF -->|extract_text| TXT["media:enc=utf-8"]
TXT -->|generate_embeddings| EMB["media:embedding"]
TXT -->|summarize| TXT2["media:enc=utf-8"]
IMG -->|describe_image| TXT
Il LiveCapFab è un grafo orientato i cui nodi sono media URN e i cui archi sono capacità. Quando viene registrata una cap con URN di input A e URN di output B, il grafo acquisisce un arco A → B etichettato con quella cap. Anche ogni altro output dichiarato dalla cap è raggiungibile: un output il cui media URN è C aggiunge un arco A → C, etichettato con la cap e con quell’output. Il tipo a runtime dell’output principale segue l’effetto del cap URN (vedi Struttura dei cap URN); ogni altro output dà il proprio tipo. Un output che porta l’input della cap invariato (quello di un router) non ha un tipo proprio, quindi non aggiunge alcun arco.
La ricerca dei percorsi attraversa questo grafo per scoprire catene di trasformazione in più passi: se esistono una cap A → B e una cap B → C, allora il tipo di input A può raggiungere il tipo di output C tramite una catena di due passi.
Sorgente: capdag-rs/src/planner/live_cap_fab.rs.
Destinazioni raggiungibili
get_reachable_targets() restituisce tutti i tipi di media raggiungibili da una data sorgente tramite una o più invocazioni di cap. Esegue una visita in ampiezza del grafo a partire dal nodo sorgente, raccogliendo tutti i nodi raggiungibili.
Viene usata dall’interfaccia per mostrare quali trasformazioni sono disponibili per un dato file di input (“puoi estrarre i metadati, generare una miniatura, creare embedding, …”), senza enumerare ogni percorso possibile.
Ogni risultato è un ReachableTargetInfo che contiene il media URN di destinazione e la macchina necessaria per raggiungerlo.
Ricerca dei percorsi
find_paths_to_exact_target() restituisce uno o più oggetti Strand che collegano una sorgente specifica a una destinazione specifica. Ogni strand è un percorso concreto attraverso il grafo delle capacità.
Quando esistono più percorsi (ad esempio PDF → testo tramite estrazione, oppure PDF → immagine → testo tramite OCR), vengono restituiti tutti i percorsi validi. Il chiamante sceglie quale eseguire in base a preferenze o al contesto.
Sorgente: live_cap_fab.rs.
Strand
Uno Strand è un elenco ordinato di StrandStep che rappresenta un percorso attraverso il grafo delle capacità:
[source] --cap_1--> [intermediate_1] --cap_2--> [intermediate_2] --cap_3--> [target]
StrandStep
Ogni passo ha un token_id, la sua identità stabile, coniata quando il passo viene creato; i
tipi di media ai suoi confini, from_spec e to_spec; e uno step_type:
Cap: ilcap_urnda invocare, il suotitlee la suaspecificity, se il suo input e l’output che prende sono sequenze (input_is_sequence,output_is_sequence),inputs(ogni argomento che riceve e il produttore di quell’input: l’input dello strand o un altro passo, tramite token), eoutput_urn: l’altro output che il passo prende, tramite il suo media URN, oppure nessuno per l’output principale.ForEach: una sequenza diventa i suoi elementi; il tipo di media non cambia.Collect: gli elementi diventano una sequenza; il tipo di media non cambia.
Il from_spec del primo passo è la sorgente; il to_spec dell’ultimo passo è la destinazione. Uno
strand che prende un altro output diventa una macchina il cui passo prende quell’output tramite il suo nome
nella definizione della cap (-> (rest=n)); uno strand che prende un output che la sua cap non
dichiara come altro output viene rifiutato (UnknownStrandOutput).
Sorgente: live_cap_fab.rs, machine/resolve.rs.
Analisi della cardinalità
Il modulo della cardinalità determina la forma dei dati che scorrono attraverso il piano:
InputCardinality: i dati sono un singolo elemento o un elenco?
Single: un solo valore (ad esempiomedia:document;ext=pdf, un documento PDF).Sequence: più valori (ad esempiomedia:document;ext=pdf;list, un elenco di documenti PDF). Lo segnala il tag marcatorelistnel media URN.
InputStructure: i dati sono opachi o strutturati?
Opaque: byte grezzi senza struttura interna (ad esempiomedia:binary).Record: un record strutturato con campi con nome (ad esempiomedia:record). Lo segnala il tag marcatorerecord.
MediaShape: combina cardinalità e struttura in un’unica classificazione.
ShapeCompatibility: determina se due forme sono compatibili per essere collegate. Una sorgente Single può alimentare un consumatore Single. Una sorgente Sequence può alimentare un nodo ForEach che elabora gli elementi singolarmente.
Sorgente: capdag-rs/src/planner/cardinality.rs.
Associazione degli argomenti
Il sistema di associazione degli argomenti registra, per ogni argomento di un’invocazione di cap, da dove proviene il suo valore:
ArgumentBinding |
Valore |
|---|---|
InputFile { index }, InputFilePath, InputMediaUrn |
uno dei file di input dell’esecuzione: il suo contenuto, il suo percorso o il suo media URN |
PreviousOutput { node_id } |
ciò che un nodo precedente ha prodotto |
Literal { value } |
una costante scritta dalla macchina (name = constant), un valore JSON codificato da json_value_to_bytes |
Slot { name, schema } |
un valore che l’esecuzione deve fornire per questo passo |
CapSetting { setting_urn } |
l’impostazione per cap |
CapDefault |
il default_value della definizione |
PlanMetadata { key } |
un valore portato dal piano stesso |
ArgumentBindings è la raccolta per un’invocazione, indicizzata per media URN dell’argomento.
ResolvedArgument è il valore finale: byte pronti per essere inviati, con l’ArgumentSource da cui
provengono.
Sorgente: capdag-rs/src/planner/argument_binding.rs.
MachinePlan
Un MachinePlan è un DAG di MachineNode collegati da MachinePlanEdge. È il formato di output del planner: un piano di esecuzione completamente specificato.
Sorgente: capdag-rs/src/planner/plan.rs.
Tipi di nodo
pub enum ExecutionNodeType {
Cap { token_id, cap_urn, arg_bindings, preferred_cap, policy }, // un passo: un'invocazione di una cap
CapOutput { cap_node, output_media_urn, output_name, carries_input }, // un output di un passo diverso da quello principale
ForEach { token_id, sources, body }, // una mappa: il suo corpo viene eseguito una volta per elemento delle sue sorgenti, accoppiate
Item { foreach, source, media_urn }, // ciò che un'esecuzione del corpo di una mappa riceve di una sorgente
Collect { input_nodes, output_media_urn }, // una raccolta: più valori, in ordine, come una sola sequenza
InputSlot { slot_name, expected_media_urn, cardinality }, // qualcosa che l'esecuzione fornisce
Output { output_name, source_node }, // qualcosa che l'esecuzione restituisce
}
- Un nodo
Capè un passo, e rappresenta il suo output principale: ciò che legge l’output principale si collega dal nodo del passo. Porta la politica del passo (retry,on_error) e i suoi binding degli argomenti, costanti comprese. - Ogni altro output di un passo che qualcosa prende è un nodo
CapOutput; il suo valore in un’esecuzione viene memorizzato sotto quel nodo.carries_inputcontrassegna un output che porta l’input del passo, con il tipo invariato. - Un
ForEachelenca le sequenze che accoppia (sources, ciascuna con il nodoItemcome cui il corpo riceve il suo elemento) e il suobody: ogni nodo che viene eseguito una volta per elemento. Una mappa aperta dentro un corpo è un membro di quel corpo; il suo corpo è il suo. - Un
Collectè una raccolta in un argomento sequenza. Un membro che contiene un solo valore contribuisce con un elemento, un membro che contiene una sequenza con i suoi elementi, un membro assente con nessuno. Un solo valore legato a un argomento sequenza è una raccolta di un solo membro. InputSloteOutputsono il confine del piano. Una macchina che parte da una sorgente non haInputSlot; una che termina in un pozzo non haOutput.
ForEach, Item e Collect realizzano ciò che la notazione lascia alla cardinalità
(Notazione delle macchine): nessuno di essi viene scritto.
Tipi di arco
pub enum EdgeType {
Direct, // In un passo: il suo input principale. In ogni altro nodo: ciò di cui quel nodo è
Arg { arg_urn }, // In un argomento non principale di un passo, consegnato con quell'URN
Iteration, // Da una mappa a uno dei suoi nodi elemento
Collection, // Da un nodo raccolto alla sua raccolta
}
Un arco dice che cosa viene consegnato, non quante volte. Un nodo dentro una mappa, letto dall’esterno, è
la sequenza dei suoi valori sulle esecuzioni, nell’ordine degli elementi; letto dall’interno della stessa esecuzione, è
il valore di quell’esecuzione. L’esecutore ricava quale dei due da dove si trovano i due nodi. Da quale output di un passo
parte un arco è dato dal nodo da cui parte: il nodo stesso del passo per l’output principale, un
nodo CapOutput per ogni altro. Un valore che deve essere scomposto viene scomposto da una cap; non esistono
archi che estraggono campi.
MachinePlan::validate vincola un piano a questa forma: una mappa itera almeno una sequenza, ogni
elemento è un elemento della propria mappa e un membro del suo corpo, nessun nodo sta in due corpi, un CapOutput
nomina un passo, e ogni raccolta e ogni output nomina un nodo del piano.
MachinePlanBuilder
flowchart LR
S["MachineStrand<br/>(passi risolti)"] --> PB["MachinePlanBuilder"]
SH["Input dell'esecuzione<br/>(quanti valori, di quale tipo)"] --> PB
PB --> IS["Nodi InputSlot"]
PB --> CAP["Nodi Cap, CapOutput"]
PB --> MAP["ForEach, Item, Collect"]
PB --> OUT["Nodi Output"]
IS --> MP["MachinePlan"]
CAP --> MP
MAP --> MP
OUT --> MP
Il costruttore di piani converte uno strand risolto di una Machine, e ciò che l’esecuzione lega a ciascuno dei
suoi input (AnchorInput: un valore o una sequenza, e il tipo di ciò che viene legato), in un
MachinePlan.
build_plan_from_machine_strand_for_run() è il punto d’ingresso
(build_plan_from_machine_strand() lega ogni input allo stesso modo, al suo tipo dichiarato):
- Crea un
InputSlotper ogni ancora di input dello strand, che contiene il tipo che l’esecuzione vi lega. Ogni input deve essere legato, e il tipo legato deve essere conforme al tipo dichiarato dell’input. - Decide dove viene eseguito ogni passo: la regola di iterazione di Notazione delle macchine, applicata allo strand con la forma dell’esecuzione. È la stessa regola che il resolver applica alla sola notazione, quindi una macchina mappa in un’esecuzione esattamente dove la sua notazione dice che lo farebbe.
- Crea un
ForEachper ogni mappa, con un nodoItemper sorgente, ed elenca nel suo corpo ogni nodo che viene eseguito una volta per elemento. - Crea un nodo
Capper ogni passo e un nodoCapOutputper ogni altro output che fornisce. Collega ogni binding: l’elemento, dove il passo sta dentro la mappa che itera la sua sorgente e l’argomento prende un solo valore, altrimenti il nodo stesso; unCollectdove più nodi, o un solo valore, alimentano un argomento sequenza. Una costante diventa un bindingLiterale non viene collegata da nulla. - Crea un
Outputper ogni ancora di output.
Un percorso pianificato (uno Strand dal capfab) diventa un piano allo stesso modo: viene prima risolto in uno
MachineStrand.
Sorgente: capdag-rs/src/planner/plan_builder.rs, capdag-rs/src/machine/iteration.rs.
Priorità di risoluzione degli slot
flowchart TD
Q{"slot_values<br/>[token_id:slot_name]?"} -->|Trovato| V1["Usa il valore esplicito<br/>per passo"]
Q -->|Non trovato| Q2{"cap_settings<br/>[cap_urn][slot_name]?"}
Q2 -->|Trovato| V2["Usa l'impostazione<br/>per cap"]
Q2 -->|Non trovato| Q3{"default_value<br/>nella definizione della cap?"}
Q3 -->|Trovato| V3["Usa il valore predefinito"]
Q3 -->|Non trovato| V4["Nessun valore<br/>(argomento omesso)"]
Un argomento che la macchina lega, a un altro nodo o a una costante, non è uno slot: il suo valore appartiene alla macchina, e una voce di slot_values o un aggiornamento a metà esecuzione indirizzato a esso viene rifiutato, nominando il passo e l’argomento. Per un argomento che la macchina lascia non legato, il costruttore risolve il suo valore in tre passi:
slot_values["{token_id}:{slot_name}"]: valori espliciti per passo, indirizzati dalloStepTokendel passo (loStrandStep::token_idconiato dal planner, che è anche l’id del nodo cap del piano). Un token viene dal piano oppure non esiste: non viene mai derivato da una posizione, perché uno strand è un DAG: rami paralleli che confluiscono a valle non hanno un ordinale, e due cap identiche su rami separati differiscono solo per il token.cap_settings[cap_urn][slot_name]: impostazioni per cap. Si applicano a ogni istanza di una cap, indipendentemente dal passo in cui compare.default_valuedalla definizione della cap: il ripiego.
Questo ordine di priorità consente agli utenti di sovrascrivere passi specifici pur mantenendo valori predefiniti sensati.
Sorgente: plan_builder.rs.
Che cosa viene chiesto a un’esecuzione di un piano
MachinePlanBuilder::analyze_plan_arguments() legge un piano e dice, per ogni passo (indirizzato dal
suo token) e ogni argomento della sua cap, da dove proviene il valore dell’argomento:
ArgumentResolution |
L’argomento |
|---|---|
FromInput |
è collegato da un input della macchina (attraverso l’elemento di una mappa, o una raccolta di input) |
FromStep |
è collegato da ciò che produce un altro passo |
Constant |
è legato a una costante scritta dalla macchina |
HasDefault |
è lasciato non legato; la sua definizione dà un valore predefinito |
RequiresUserInput |
è lasciato non legato e non ha un valore predefinito |
Gli ultimi due sono gli slot del passo: gli argomenti a cui un’esecuzione può dare un valore. Un’esecuzione può fare a meno dell’utente quando nessuno slot è obbligatorio e privo di valore predefinito. L’analisi legge gli stessi collegamenti e binding creati dal costruttore di piani, quindi un argomento legato da una costante o da una sorgente con nome non viene mai chiesto. I mirror portano i tipi del risultato; i piani vengono costruiti, e analizzati, solo dall’implementazione di riferimento.
Sorgente: plan_builder.rs.
Eseguire un piano
Un piano viene eseguito da execute_plan (Esecuzione). Non esiste un altro esecutore:
il planner produce piani e non li esegue.
cap_step_count
Il cap_step_count di un percorso pianificato conta solo i suoi passi Cap: le transizioni di cardinalità non sono
passi che un utente vede, quindi i candidati vengono classificati e mostrati in base a esso.
Sorgente: capdag-rs/src/planner/live_cap_fab.rs.
I feed live non richiedono supporto dal planner
Una sorgente di feed live (13.2 §Media di riferimento) non richiede nulla al
planner. La dichiarazione is_sequence dell’argomento consumatore
prende ogni decisione di wiring: una cap che consuma sequenze (concat)
si collega direttamente al produttore della sequenza e riceve lo stream intero;
su un feed illimitato questo è un piano valido a emissione differita, poiché
l’arresto dell’esecuzione consegna END e il ripiegamento emette (un concat su un
microfono è “l’intera registrazione, una volta fermata”); una cap che consuma elementi
(un filtro per fotogramma) viene mappata, esattamente come per qualsiasi sequenza
limitata. La tempistica dell’emissione è una politica dell’operazione, non una struttura di tipo, e non esiste
alcuna restrizione “illimitato” al momento della pianificazione, né l’illimitatezza compare
nel piano: un piano è un piano sia che la sua sorgente sia un file sia un microfono.
La limitatezza è invece una questione dell’esecutore, decisa dalle dichiarazioni
streaming delle definizioni delle cap (15.2 §Contratti di streaming): un passaggio
da un produttore che può emettere senza una promessa di lunghezza a un consumatore
che non consuma senza di essa è un confine di catena: il produttore viene eseguito
fino al completamento, il suo output viene accumulato su disco, e al consumatore viene fornito l’intero limitato
dopo END. Il piano non cambia mai per questo; cambia solo il modo in cui la catena viene
eseguita. La memoria per le operazioni che richiedono il completamento su feed lunghi è una
questione operativa dell’operazione (buffer, riversamento, finestra), resa visibile tramite
i normali meccanismi di pressione della memoria, mai un errore del planner.