1786 lines
154 KiB
HTML
1786 lines
154 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="es" data-theme="dark">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="description" content="Walkthrough completo de AgentForge — de alto a bajo nivel, con diagramas.">
|
||
<title>AgentForge · Walkthrough</title>
|
||
<script>
|
||
(function () {
|
||
try {
|
||
var t = localStorage.getItem('af-theme');
|
||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||
document.documentElement.setAttribute('data-theme', t);
|
||
} catch (e) { document.documentElement.setAttribute('data-theme', 'dark'); }
|
||
})();
|
||
</script>
|
||
<style>
|
||
:root, [data-theme="dark"]{
|
||
--bg:#0d1117; --bg-soft:#0f141b; --surface:#161b22; --surface-2:#1b222b; --surface-3:#212a34;
|
||
--border:#2d333b; --border-soft:#22272e; --text:#e6edf3; --text-soft:#c9d1d9; --muted:#8b949e;
|
||
--accent:#6ea8fe; --accent-2:#a78bfa; --accent-3:#f0883e; --ok:#3fb950; --warn:#d29922; --danger:#f85149; --info:#58a6ff;
|
||
--code-bg:#0b0f14; --code-border:#222b35;
|
||
--shadow:0 1px 0 rgba(0,0,0,.25), 0 8px 28px rgba(0,0,0,.35);
|
||
--radius:14px; --radius-sm:9px;
|
||
--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
|
||
--sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,system-ui,sans-serif;
|
||
}
|
||
[data-theme="light"]{
|
||
--bg:#ffffff; --bg-soft:#f6f8fa; --surface:#ffffff; --surface-2:#f6f8fa; --surface-3:#eef1f4;
|
||
--border:#d0d7de; --border-soft:#e4e8ec; --text:#1f2328; --text-soft:#373d44; --muted:#636c76;
|
||
--accent:#0969da; --accent-2:#8250df; --accent-3:#bc4c00; --ok:#1a7f37; --warn:#9a6700; --danger:#cf222e; --info:#0969da;
|
||
--code-bg:#f6f8fa; --code-border:#d8dee4;
|
||
--shadow:0 1px 0 rgba(31,35,40,.04), 0 8px 24px rgba(31,35,40,.08);
|
||
}
|
||
*{box-sizing:border-box}
|
||
html{scroll-behavior:smooth}
|
||
body{
|
||
margin:0; background:var(--bg); color:var(--text); font-family:var(--sans);
|
||
font-size:16px; line-height:1.65; -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
|
||
}
|
||
a{color:var(--accent); text-decoration:none}
|
||
a:hover{text-decoration:underline}
|
||
code,kbd,pre{font-family:var(--mono)}
|
||
.layout{display:grid; grid-template-columns:300px minmax(0,1fr); align-items:start}
|
||
/* ---------- Sidebar ---------- */
|
||
.sidebar{
|
||
position:sticky; top:0; height:100vh; overflow:auto; padding:22px 14px 40px 22px;
|
||
border-right:1px solid var(--border-soft); background:linear-gradient(180deg,var(--bg-soft),var(--bg));
|
||
}
|
||
.brand{display:flex; gap:11px; align-items:center; margin-bottom:6px}
|
||
.brand .logo{font-size:26px; line-height:1}
|
||
.brand .t1{font-weight:700; font-size:17px; letter-spacing:.2px}
|
||
.brand .t2{font-size:11.5px; color:var(--muted); margin-top:1px}
|
||
.sidebar .meta{font-size:11.5px; color:var(--muted); margin:10px 0 16px; border-top:1px dashed var(--border); padding-top:12px}
|
||
.toc{list-style:none; margin:0; padding:0; font-size:13.5px}
|
||
.toc li{margin:1px 0}
|
||
.toc .group{margin:14px 0 5px; font-size:10.5px; letter-spacing:.13em; text-transform:uppercase; color:var(--muted); font-weight:700}
|
||
.toc a{display:block; padding:5px 10px; border-radius:7px; color:var(--text-soft); border-left:2px solid transparent}
|
||
.toc a:hover{background:var(--surface); text-decoration:none; color:var(--text)}
|
||
.toc a.active{background:var(--surface-2); color:var(--text); border-left-color:var(--accent); font-weight:600}
|
||
.toc a .n{color:var(--muted); font-variant-numeric:tabular-nums; margin-right:7px; font-size:12px}
|
||
/* ---------- Main ---------- */
|
||
main{padding:0 clamp(16px,3.4vw,52px) 120px; max-width:1080px; margin:0 auto; width:100%}
|
||
section{scroll-margin-top:18px; padding-top:34px}
|
||
section > h2{
|
||
font-size:clamp(22px,2.6vw,30px); line-height:1.25; margin:0 0 4px; padding-bottom:9px; border-bottom:1px solid var(--border-soft);
|
||
scroll-margin-top:18px; display:flex; align-items:baseline; gap:11px; flex-wrap:wrap;
|
||
}
|
||
section > h2 .kicker{font-size:12px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--accent)}
|
||
h3{font-size:18.5px; margin:30px 0 6px; line-height:1.3}
|
||
h4{font-size:15px; margin:22px 0 4px; color:var(--text-soft); letter-spacing:.01em}
|
||
p{margin:10px 0}
|
||
.lead{font-size:17.5px; color:var(--text-soft)}
|
||
.muted{color:var(--muted)}
|
||
hr{border:0; border-top:1px solid var(--border-soft); margin:34px 0}
|
||
ul,ol{padding-left:22px}
|
||
li{margin:4px 0}
|
||
strong{color:var(--text); font-weight:650}
|
||
em{color:var(--text-soft)}
|
||
/* ---------- Hero ---------- */
|
||
.hero{
|
||
margin-top:30px; padding:30px 30px 26px; border:1px solid var(--border); border-radius:var(--radius);
|
||
background:
|
||
radial-gradient(820px 280px at 88% -40%, color-mix(in srgb,var(--accent-2) 24%,transparent), transparent 70%),
|
||
radial-gradient(680px 280px at 6% 120%, color-mix(in srgb,var(--accent) 18%,transparent), transparent 70%),
|
||
linear-gradient(180deg,var(--surface),var(--bg-soft));
|
||
box-shadow:var(--shadow); position:relative; overflow:hidden;
|
||
}
|
||
.hero h1{font-size:clamp(28px,4.3vw,42px); line-height:1.12; margin:6px 0 8px; letter-spacing:-.01em}
|
||
.hero .tagline{font-size:clamp(15px,1.9vw,18.5px); color:var(--text-soft); max-width:62ch; margin:0}
|
||
.hero .crumbs{font-size:12px; color:var(--muted); margin-bottom:2px}
|
||
.badges{display:flex; flex-wrap:wrap; gap:7px; margin-top:18px}
|
||
.badge{
|
||
font-size:11.5px; padding:3.5px 10px; border-radius:999px; border:1px solid var(--border); background:var(--surface-2);
|
||
color:var(--text-soft); display:inline-flex; gap:6px; align-items:center; white-space:nowrap;
|
||
}
|
||
.badge b{color:var(--text); font-weight:650}
|
||
.badge.k{border-color:color-mix(in srgb,var(--accent) 50%,var(--border)); background:color-mix(in srgb,var(--accent) 12%,var(--surface-2))}
|
||
.hero .quick{display:flex; gap:10px; flex-wrap:wrap; margin-top:20px}
|
||
.btn{
|
||
display:inline-flex; align-items:center; gap:8px; font-size:13.5px; font-weight:600; padding:8px 14px; border-radius:9px;
|
||
border:1px solid var(--border); background:var(--surface-2); color:var(--text); cursor:pointer;
|
||
}
|
||
.btn:hover{text-decoration:none; border-color:var(--accent); color:var(--text)}
|
||
.btn.primary{background:color-mix(in srgb,var(--accent) 20%,var(--surface)); border-color:color-mix(in srgb,var(--accent) 55%,var(--border))}
|
||
/* ---------- Topbar (mobile) ---------- */
|
||
.topbar{display:none; position:sticky; top:0; z-index:30; background:color-mix(in srgb,var(--bg) 86%,transparent); backdrop-filter:blur(8px); border-bottom:1px solid var(--border-soft); padding:9px 14px; align-items:center; gap:10px}
|
||
.topbar .tt{font-weight:700; font-size:14px}
|
||
.icon-btn{border:1px solid var(--border); background:var(--surface-2); color:var(--text); border-radius:8px; width:34px; height:34px; display:inline-grid; place-items:center; cursor:pointer; font-size:15px}
|
||
/* ---------- Callouts ---------- */
|
||
.callout{border:1px solid var(--border); border-left-width:4px; border-radius:var(--radius-sm); padding:13px 16px; margin:18px 0; background:var(--surface); font-size:14.5px}
|
||
.callout .ct{font-weight:700; font-size:12px; letter-spacing:.06em; text-transform:uppercase; display:flex; gap:8px; align-items:center; margin-bottom:3px}
|
||
.callout p{margin:5px 0}
|
||
.callout.tip{border-left-color:var(--accent)} .callout.tip .ct{color:var(--accent)}
|
||
.callout.key{border-left-color:var(--accent-2); background:color-mix(in srgb,var(--accent-2) 8%,var(--surface))} .callout.key .ct{color:var(--accent-2)}
|
||
.callout.warn{border-left-color:var(--warn); background:color-mix(in srgb,var(--warn) 9%,var(--surface))} .callout.warn .ct{color:var(--warn)}
|
||
.callout.ok{border-left-color:var(--ok); background:color-mix(in srgb,var(--ok) 9%,var(--surface))} .callout.ok .ct{color:var(--ok)}
|
||
/* ---------- Tables ---------- */
|
||
.tbl-wrap{overflow:auto; margin:18px 0; border:1px solid var(--border); border-radius:var(--radius-sm)}
|
||
table{border-collapse:collapse; width:100%; font-size:13.8px}
|
||
th,td{text-align:left; padding:9px 13px; border-bottom:1px solid var(--border-soft); vertical-align:top}
|
||
thead th{background:var(--surface-2); color:var(--text); font-weight:650; position:sticky; top:0; font-size:12.5px; letter-spacing:.02em; border-bottom:1px solid var(--border)}
|
||
tbody tr:last-child td{border-bottom:0}
|
||
tbody tr:hover{background:color-mix(in srgb,var(--accent) 4%,transparent)}
|
||
td code, th code{font-size:12.5px}
|
||
/* ---------- Code ---------- */
|
||
.code{margin:18px 0; border:1px solid var(--code-border); border-radius:var(--radius-sm); background:var(--code-bg); overflow:hidden}
|
||
.code .hd{display:flex; align-items:center; gap:9px; padding:7px 12px; border-bottom:1px solid var(--code-border); background:color-mix(in srgb,var(--surface-2) 60%,var(--code-bg)); font-size:12px; color:var(--muted)}
|
||
.code .hd .dot{width:9px;height:9px;border-radius:50%;background:var(--border)}
|
||
.code .hd .fn{font-family:var(--mono); color:var(--text-soft)}
|
||
.code .hd .lang{margin-left:auto; font-size:10.5px; letter-spacing:.1em; text-transform:uppercase}
|
||
.code .cp{margin-left:8px; cursor:pointer; border:1px solid var(--code-border); border-radius:6px; padding:1px 7px; font-size:10.5px; color:var(--muted); background:transparent}
|
||
.code .cp:hover{color:var(--text); border-color:var(--border)}
|
||
.code pre{margin:0; padding:13px 15px; overflow:auto; font-size:12.9px; line-height:1.6; color:var(--text-soft)}
|
||
.code pre .c{color:var(--muted); font-style:italic} /* comment */
|
||
.code pre .k{color:var(--accent-2)} /* keyword */
|
||
.code pre .s{color:#7ee787} /* string */
|
||
[data-theme="light"] .code pre .s{color:#0a7d33}
|
||
.code pre .n{color:var(--accent-3)} /* number/literal */
|
||
.code pre .y{color:var(--accent)} /* yaml key / fn name */
|
||
.code pre .d{color:var(--danger)} /* danger token */
|
||
p code, li code, td code{background:var(--surface-2); border:1px solid var(--border-soft); padding:.5px 5px; border-radius:5px; font-size:.86em}
|
||
/* ---------- Card grid ---------- */
|
||
.cards{display:grid; grid-template-columns:repeat(auto-fit,minmax(255px,1fr)); gap:14px; margin:18px 0}
|
||
.card{border:1px solid var(--border); border-radius:var(--radius-sm); padding:15px 16px; background:var(--surface); position:relative}
|
||
.card .idx{font-size:11px; font-weight:800; color:var(--accent-2); letter-spacing:.06em}
|
||
.card h4{margin:4px 0 5px; font-size:15.5px; color:var(--text)}
|
||
.card p{margin:0; font-size:13.6px; color:var(--text-soft)}
|
||
.card .where{margin-top:9px; font-size:11.5px; color:var(--muted); font-family:var(--mono)}
|
||
/* ---------- Pills / kbd ---------- */
|
||
.pill{display:inline-block; font-size:11px; padding:1.5px 8px; border-radius:999px; border:1px solid var(--border); background:var(--surface-2); color:var(--text-soft); font-family:var(--mono)}
|
||
.pill.ok{color:var(--ok); border-color:color-mix(in srgb,var(--ok) 45%,var(--border)); background:color-mix(in srgb,var(--ok) 12%,var(--surface))}
|
||
.pill.warn{color:var(--warn); border-color:color-mix(in srgb,var(--warn) 45%,var(--border)); background:color-mix(in srgb,var(--warn) 12%,var(--surface))}
|
||
.pill.danger{color:var(--danger); border-color:color-mix(in srgb,var(--danger) 45%,var(--border)); background:color-mix(in srgb,var(--danger) 12%,var(--surface))}
|
||
.pill.accent{color:var(--accent); border-color:color-mix(in srgb,var(--accent) 45%,var(--border)); background:color-mix(in srgb,var(--accent) 12%,var(--surface))}
|
||
.pill.violet{color:var(--accent-2); border-color:color-mix(in srgb,var(--accent-2) 45%,var(--border)); background:color-mix(in srgb,var(--accent-2) 12%,var(--surface))}
|
||
/* ---------- Diagrams ---------- */
|
||
figure.diagram{margin:22px 0; border:1px solid var(--border); border-radius:var(--radius); background:
|
||
radial-gradient(700px 200px at 100% 0%, color-mix(in srgb,var(--accent) 7%,transparent), transparent 60%),
|
||
var(--surface); padding:16px 16px 8px; box-shadow:var(--shadow)}
|
||
figure.diagram svg{width:100%; height:auto; display:block}
|
||
figure.diagram figcaption{font-size:12.5px; color:var(--muted); padding:9px 4px 6px; border-top:1px dashed var(--border); margin-top:6px}
|
||
figure.diagram figcaption b{color:var(--text-soft)}
|
||
.legend{display:flex; flex-wrap:wrap; gap:7px 14px; font-size:11.5px; color:var(--muted); margin:4px 2px 12px; align-items:center}
|
||
.legend .li{display:inline-flex; align-items:center; gap:6px}
|
||
.legend .sw{width:13px; height:13px; border-radius:4px; border:1px solid var(--border)}
|
||
.sw.box{background:var(--surface); border-color:var(--border)}
|
||
.sw.accent{background:color-mix(in srgb,var(--accent) 16%,var(--surface)); border-color:var(--accent)}
|
||
.sw.violet{background:color-mix(in srgb,var(--accent-2) 16%,var(--surface)); border-color:var(--accent-2)}
|
||
.sw.warn{background:color-mix(in srgb,var(--warn) 18%,var(--surface)); border-color:var(--warn)}
|
||
.sw.danger{background:color-mix(in srgb,var(--danger) 16%,var(--surface)); border-color:var(--danger)}
|
||
.sw.ok{background:color-mix(in srgb,var(--ok) 16%,var(--surface)); border-color:var(--ok)}
|
||
/* SVG element classes */
|
||
.dg-frame{fill:transparent; stroke:var(--border); stroke-dasharray:7 6}
|
||
.dg-frame-lbl{fill:var(--muted); font:700 12px var(--sans); letter-spacing:.06em}
|
||
.dg-box{fill:var(--surface); stroke:var(--border); stroke-width:1.4}
|
||
.dg-box.soft{fill:var(--surface-2)}
|
||
.dg-box.accent{stroke:var(--accent); fill:color-mix(in srgb,var(--accent) 11%,var(--surface))}
|
||
.dg-box.violet{stroke:var(--accent-2); fill:color-mix(in srgb,var(--accent-2) 11%,var(--surface))}
|
||
.dg-box.ok{stroke:var(--ok); fill:color-mix(in srgb,var(--ok) 12%,var(--surface))}
|
||
.dg-box.warn{stroke:var(--warn); fill:color-mix(in srgb,var(--warn) 13%,var(--surface))}
|
||
.dg-box.danger{stroke:var(--danger); fill:color-mix(in srgb,var(--danger) 11%,var(--surface))}
|
||
.dg-box.dashed{stroke-dasharray:6 5}
|
||
.dg-t{fill:var(--text); font:650 14px var(--sans)}
|
||
.dg-t.sm{font-size:12.5px; font-weight:600}
|
||
.dg-s{fill:var(--muted); font:400 11.5px var(--sans)}
|
||
.dg-m{fill:var(--text-soft); font:400 11.5px var(--mono)}
|
||
.dg-m.dim{fill:var(--muted); font-size:11px}
|
||
.dg-edge{stroke:var(--muted); stroke-width:1.7; fill:none}
|
||
.dg-edge.accent{stroke:var(--accent)} .dg-edge.violet{stroke:var(--accent-2)} .dg-edge.ok{stroke:var(--ok)}
|
||
.dg-edge.danger{stroke:var(--danger)} .dg-edge.warn{stroke:var(--warn)}
|
||
.dg-edge.dashed{stroke-dasharray:6 5} .dg-edge.thin{stroke-width:1.2}
|
||
.dg-el{fill:var(--muted); font:600 11px var(--sans)}
|
||
.dg-el.bg{paint-order:stroke; stroke:var(--surface); stroke-width:4px; stroke-linejoin:round}
|
||
.dg-life{stroke:var(--border); stroke-width:1.3; stroke-dasharray:3 4}
|
||
.dg-actor{fill:var(--surface-2); stroke:var(--border); stroke-width:1.3}
|
||
.dg-band{fill:color-mix(in srgb,var(--accent-2) 7%,transparent)}
|
||
.dg-divider{stroke:var(--border); stroke-dasharray:5 5; stroke-width:1.2}
|
||
/* ---------- CSS-flow diagrams ---------- */
|
||
.flow{display:flex; align-items:stretch; gap:0; flex-wrap:wrap; margin:18px 0; counter-reset:fl}
|
||
.flow .step{flex:1 1 150px; min-width:140px; border:1px solid var(--border); background:var(--surface); border-radius:var(--radius-sm); padding:12px 13px; position:relative}
|
||
.flow .step .st{font-size:13.5px; font-weight:650}
|
||
.flow .step .sd{font-size:11.5px; color:var(--muted); margin-top:2px; font-family:var(--mono)}
|
||
.flow .arr{display:flex; align-items:center; padding:0 7px; color:var(--muted); font-size:20px}
|
||
.flow.v{flex-direction:column; align-items:stretch}
|
||
.flow.v .arr{justify-content:center; padding:5px 0; transform:rotate(90deg); width:max-content; align-self:center}
|
||
.bands{margin:18px 0; border:1px solid var(--border); border-radius:var(--radius); overflow:hidden}
|
||
.band{display:grid; grid-template-columns:96px 1fr; border-bottom:1px solid var(--border-soft)}
|
||
.band:last-child{border-bottom:0}
|
||
.band:nth-child(odd){background:var(--surface)}
|
||
.band:nth-child(even){background:var(--surface-2)}
|
||
.band .lvl{display:flex; flex-direction:column; align-items:center; justify-content:center; gap:2px; background:color-mix(in srgb,var(--accent) 9%,var(--surface)); border-right:1px solid var(--border-soft); font-size:11px; color:var(--muted); padding:8px 6px; text-align:center}
|
||
.band .lvl b{font-size:17px; color:var(--accent); line-height:1}
|
||
.band .mods{display:flex; flex-wrap:wrap; gap:6px; padding:11px 13px; align-items:center}
|
||
.band .desc{flex-basis:100%; font-size:11.5px; color:var(--muted); margin-top:1px}
|
||
.mod{font-family:var(--mono); font-size:11.8px; padding:3px 8px; border-radius:6px; border:1px solid var(--border); background:var(--bg-soft); color:var(--text-soft)}
|
||
.mod.core{border-color:var(--accent); color:var(--accent)}
|
||
/* ---------- two-col ---------- */
|
||
.cols{display:grid; grid-template-columns:1fr 1fr; gap:18px; margin:18px 0}
|
||
.cols > div{min-width:0}
|
||
@media (max-width:760px){.cols{grid-template-columns:1fr}}
|
||
/* ---------- details ---------- */
|
||
details.dd{border:1px solid var(--border); border-radius:var(--radius-sm); margin:16px 0; background:var(--surface); overflow:hidden}
|
||
details.dd > summary{cursor:pointer; padding:11px 15px; font-weight:650; font-size:14px; list-style:none; display:flex; align-items:center; gap:9px; background:var(--surface-2)}
|
||
details.dd > summary::-webkit-details-marker{display:none}
|
||
details.dd > summary::before{content:"▸"; color:var(--muted); transition:transform .15s}
|
||
details.dd[open] > summary::before{transform:rotate(90deg)}
|
||
details.dd .body{padding:4px 16px 14px}
|
||
/* ---------- back to top ---------- */
|
||
#totop{position:fixed; right:18px; bottom:18px; width:42px; height:42px; border-radius:50%; border:1px solid var(--border); background:var(--surface); color:var(--text); cursor:pointer; box-shadow:var(--shadow); display:none; place-items:center; font-size:18px; z-index:40}
|
||
#totop.show{display:grid}
|
||
/* ---------- footer ---------- */
|
||
.footer{margin-top:60px; padding-top:22px; border-top:1px solid var(--border-soft); font-size:13px; color:var(--muted)}
|
||
/* ---------- responsive ---------- */
|
||
@media (max-width:980px){
|
||
.layout{grid-template-columns:1fr}
|
||
.topbar{display:flex}
|
||
.sidebar{position:fixed; left:0; top:0; bottom:0; width:300px; max-width:84vw; z-index:50; transform:translateX(-105%); transition:transform .22s ease; box-shadow:var(--shadow)}
|
||
.sidebar.open{transform:none}
|
||
.scrim{position:fixed; inset:0; background:rgba(0,0,0,.5); z-index:45; display:none}
|
||
.scrim.show{display:block}
|
||
main{padding-top:6px}
|
||
}
|
||
@media (min-width:981px){ .topbar,.scrim{display:none} }
|
||
@media print{
|
||
.sidebar,.topbar,#totop,.code .cp,.scrim{display:none!important}
|
||
.layout{grid-template-columns:1fr}
|
||
body{font-size:11pt}
|
||
figure.diagram,.code,.tbl-wrap,.callout,details.dd{break-inside:avoid}
|
||
section{padding-top:14px}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="topbar">
|
||
<button class="icon-btn" id="menuBtn" aria-label="Abrir índice">☰</button>
|
||
<span class="tt">🛡️ AgentForge · Walkthrough</span>
|
||
<button class="icon-btn" id="themeBtnM" aria-label="Cambiar tema" style="margin-left:auto">◐</button>
|
||
</div>
|
||
<div class="scrim" id="scrim"></div>
|
||
|
||
<div class="layout">
|
||
<!-- ============ SIDEBAR ============ -->
|
||
<aside class="sidebar" id="sidebar">
|
||
<div class="brand">
|
||
<span class="logo">🛡️</span>
|
||
<span>
|
||
<span class="t1">AgentForge</span>
|
||
<span class="t2">Walkthrough · de alto a bajo nivel</span>
|
||
</span>
|
||
</div>
|
||
<div style="display:flex; gap:8px; margin-top:6px">
|
||
<button class="btn" id="themeBtn" style="flex:1; justify-content:center">◐ Tema</button>
|
||
<button class="btn" onclick="window.print()" style="flex:1; justify-content:center">⎙ Imprimir</button>
|
||
</div>
|
||
<div class="meta">HTML autocontenido · <code>docs/walkthrough.html</code> · AgentForge v0.1.0</div>
|
||
<nav>
|
||
<ul class="toc" id="toc">
|
||
<li class="group">Panorama</li>
|
||
<li><a href="#intro"><span class="n">00</span>Qué es y por qué</a></li>
|
||
<li><a href="#ideas"><span class="n">01</span>Las seis ideas grandes</a></li>
|
||
<li><a href="#vista"><span class="n">02</span>Vista de pájaro: 2 servicios</a></li>
|
||
<li><a href="#capas"><span class="n">03</span>Las capas del core</a></li>
|
||
<li class="group">Cómo encaja</li>
|
||
<li><a href="#modulos"><span class="n">04</span>Grafo de módulos</a></li>
|
||
<li><a href="#patron"><span class="n">05</span>Strategy + Factory + Settings</a></li>
|
||
<li><a href="#dominio"><span class="n">06</span>El dominio (modelo de datos)</a></li>
|
||
<li><a href="#guardrails"><span class="n">07</span>Guardrails: el motor</a></li>
|
||
<li><a href="#runtime"><span class="n">08</span>El grafo de ejecución</a></li>
|
||
<li><a href="#viaje"><span class="n">09</span>El viaje de una petición</a></li>
|
||
<li class="group">Bajo nivel</li>
|
||
<li><a href="#versionado"><span class="n">10</span>Versionado tipo Git</a></li>
|
||
<li><a href="#persistencia"><span class="n">11</span>Persistencia: 4 formas</a></li>
|
||
<li><a href="#observabilidad"><span class="n">12</span>Trazabilidad</a></li>
|
||
<li><a href="#estados"><span class="n">13</span>Estados de una ejecución</a></li>
|
||
<li><a href="#api"><span class="n">14</span>Referencia de la API</a></li>
|
||
<li><a href="#agente"><span class="n">15</span>El agente de ejemplo</a></li>
|
||
<li><a href="#dashboard"><span class="n">16</span>El dashboard</a></li>
|
||
<li class="group">Operar y leer</li>
|
||
<li><a href="#arranque"><span class="n">17</span>Arranque y configuración</a></li>
|
||
<li><a href="#tests"><span class="n">18</span>Tests</a></li>
|
||
<li><a href="#endpoint"><span class="n">19</span>Este documento</a></li>
|
||
<li><a href="#glosario"><span class="n">20</span>Glosario</a></li>
|
||
<li><a href="#roadmap"><span class="n">21</span>Qué no hace (a propósito)</a></li>
|
||
<li><a href="#leer"><span class="n">22</span>Por dónde empezar a leer</a></li>
|
||
<li><a href="#codificar"><span class="n">23</span>Ruta de codificación manual</a></li>
|
||
</ul>
|
||
</nav>
|
||
</aside>
|
||
|
||
<!-- ============ MAIN ============ -->
|
||
<main>
|
||
|
||
<header class="hero">
|
||
<div class="crumbs">Plataforma de gobernanza de agentes IA · documento generado para entender el proyecto completo</div>
|
||
<h1>🛡️ AgentForge — Walkthrough</h1>
|
||
<p class="tagline">Catalogación y versionado de agentes y políticas, guardrails en runtime, ejecución <em>stateful</em> con Human-in-the-Loop, observabilidad y trazabilidad de extremo a extremo. Aquí está todo: <strong>de la vista de pájaro al cableado de cada módulo</strong>, con diagramas.</p>
|
||
<div class="badges">
|
||
<span class="badge k">🐍 <b>Python 3.11+</b></span>
|
||
<span class="badge">⚡ <b>FastAPI</b> · core :8000</span>
|
||
<span class="badge">📊 <b>Streamlit</b> · dashboard :8501</span>
|
||
<span class="badge">🔀 <b>LangGraph</b> · runtime stateful</span>
|
||
<span class="badge">🛡️ <b>Guardrails-AI + Presidio</b></span>
|
||
<span class="badge">📦 <b>Pydantic v2</b> · dominio</span>
|
||
<span class="badge">🗃️ YAML · JSON · JSONL · SQLite</span>
|
||
<span class="badge">🐳 <b>docker-compose</b></span>
|
||
</div>
|
||
<div class="quick">
|
||
<a class="btn primary" href="#ideas">▶ Empezar por las 6 ideas</a>
|
||
<a class="btn" href="#runtime">El grafo de ejecución</a>
|
||
<a class="btn" href="#viaje">El viaje de una petición</a>
|
||
<a class="btn" href="#leer">Ruta de lectura (1 h)</a>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="intro">
|
||
<h2><span class="kicker">00</span> Qué es y por qué</h2>
|
||
<p class="lead">Poner agentes de IA en producción <strong>sin una capa de gobierno</strong> produce sistemas opacos: prompts que cambian sin historial, validaciones inconsistentes, acciones de alto impacto sin supervisión y ninguna auditoría de lo que decidió el agente.</p>
|
||
<p><strong>AgentForge es el "plano de control" que pones <em>delante</em> de tus agentes</strong> antes de dejarlos tocar nada importante. No es un framework para <em>construir</em> agentes; es la capa que los <strong>cataloga, versiona, valida, ejecuta de forma supervisada y audita</strong>.</p>
|
||
<div class="callout key">
|
||
<div class="ct">🧭 El caso de ejemplo del repo</div>
|
||
<p>Un agente de operaciones de telco — <code>incident_analyzer</code>: recibe la descripción de un incidente de plataforma de voz (caída de registros SIP, degradación de MOS, saturación de HSS…) y propone acciones con <strong>análisis de riesgo</strong> y <strong>plan de rollback</strong>. Las acciones de riesgo alto quedan <strong>pausadas esperando aprobación humana</strong>. Todo queda registrado con un <code>trace_id</code>.</p>
|
||
</div>
|
||
<p>Este documento va de lo general a lo concreto: primero las ideas y la forma del sistema, luego cada módulo y sus interrelaciones, y por último el cableado de bajo nivel (grafo, persistencia, API). Si solo quieres arrancarlo, ve al <code>README.md</code>; si quieres la narrativa completa, <code>docs/explicacion.md</code>; si quieres firmas exactas y cadenas de llamada, <code>docs/componentes.md</code>.</p>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="ideas">
|
||
<h2><span class="kicker">01</span> Las seis ideas grandes</h2>
|
||
<p>Si entiendes estas seis ideas, entiendes el proyecto. Todo lo demás son detalles de implementación.</p>
|
||
<div class="cards">
|
||
<div class="card">
|
||
<div class="idx">IDEA 1</div>
|
||
<h4>Agentes y políticas como ficheros declarativos, versionados como Git</h4>
|
||
<p>Un agente es un YAML (prompt, modelo, esquema de salida, umbral de aprobación). Cambias el YAML → nueva versión, con hash SHA-256 y diff legible. Sin redeploy.</p>
|
||
<div class="where">agents/ · policies/ · registry/</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="idx">IDEA 2</div>
|
||
<h4>Los guardrails son una <em>política</em>, no código disperso</h4>
|
||
<p>Una política lista validadores de entrada y de salida con su configuración. El motor los aplica; "qué se valida" es configuración, no código.</p>
|
||
<div class="where">policies/ · guardrails/</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="idx">IDEA 3</div>
|
||
<h4>La ejecución del agente es un grafo de estados con checkpoints</h4>
|
||
<p>No es "llama al LLM y ya": validar entrada → razonar → validar salida → proponer acciones → puerta de aprobación → finalizar. Cada paso se persiste.</p>
|
||
<div class="where">runtime/ (LangGraph)</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="idx">IDEA 4</div>
|
||
<h4>Human-in-the-Loop de verdad</h4>
|
||
<p>Si el agente propone algo arriesgado, el grafo <strong>se pausa</strong> a mitad de ejecución, el estado se guarda en disco, y se reanuda más tarde —incluso tras reiniciar el proceso— cuando un humano aprueba o rechaza.</p>
|
||
<div class="where">nodo approve_gate · /approve · /reject</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="idx">IDEA 5</div>
|
||
<h4>Trazabilidad obligatoria</h4>
|
||
<p>Cada petición lleva un <code>trace_id</code> (UUID) que se propaga por el middleware → los logs → la API → los ficheros de auditoría. Cada paso del grafo deja una entrada en el <code>decision_path</code> con su duración.</p>
|
||
<div class="where">observability/ · domain/execution.py</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="idx">IDEA 6</div>
|
||
<h4>Todo lo "intercambiable" está detrás de una interfaz + un factory</h4>
|
||
<p>El proveedor de LLM, el motor de guardrails, el registry… son <code>Protocol</code>s con varias implementaciones. Un factory elige cuál según la configuración. Cambias <code>.env</code>, no el código.</p>
|
||
<div class="where">llm/ · guardrails/ · registry/ (los factory.py)</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="vista">
|
||
<h2><span class="kicker">02</span> Vista de pájaro: dos servicios</h2>
|
||
<p>AgentForge son <strong>dos procesos</strong> que se hablan por HTTP/JSON, levantados por <code>docker-compose</code>:</p>
|
||
<ul>
|
||
<li><strong><code>agentforge-core</code></strong> (FastAPI, puerto <strong>8000</strong>) — todo el dominio: registry de agentes, motor de guardrails, runtime de ejecución, persistencia. <em>No tiene UI.</em></li>
|
||
<li><strong><code>agentforge-dashboard</code></strong> (Streamlit, puerto <strong>8501</strong>) — una consola visual con cinco páginas. <strong>No contiene lógica de negocio</strong>: es un cliente HTTP del core.</li>
|
||
</ul>
|
||
<p>La separación importa: el core podría servir a una CLI, a otro servicio, a un pipeline… el dashboard es solo una de las caras posibles. (Y este documento es otra cara más: el HTML que estás leyendo.)</p>
|
||
|
||
<div class="legend">
|
||
<span class="li"><span class="sw box"></span> servicio / módulo</span>
|
||
<span class="li"><span class="sw accent"></span> componente de entrada</span>
|
||
<span class="li"><span class="sw violet"></span> componente interno clave</span>
|
||
<span class="li"><span class="sw ok"></span> añadido por este walkthrough</span>
|
||
<span class="li">→ flujo de datos / dependencia</span>
|
||
</div>
|
||
|
||
<figure class="diagram">
|
||
<svg viewBox="0 0 1000 560" role="img" aria-label="Diagrama de los dos servicios y el interior del core">
|
||
<defs><marker id="ah1" markerWidth="10" markerHeight="10" refX="7.5" refY="4" orient="auto-start-reverse"><path d="M0,0 L8.5,4 L0,8 Z" fill="context-stroke"/></marker></defs>
|
||
<rect class="dg-frame" x="12" y="12" width="976" height="536" rx="18"/>
|
||
<text class="dg-frame-lbl" x="34" y="38">docker-compose · dos contenedores</text>
|
||
|
||
<!-- dashboard -->
|
||
<rect class="dg-box accent" x="56" y="60" width="300" height="106" rx="13"/>
|
||
<text class="dg-t" x="206" y="92" text-anchor="middle">agentforge-dashboard</text>
|
||
<text class="dg-s" x="206" y="112" text-anchor="middle">Streamlit · :8501 · "la consola"</text>
|
||
<text class="dg-m dim" x="206" y="132" text-anchor="middle">5 páginas · sin lógica de negocio</text>
|
||
<text class="dg-m dim" x="206" y="150" text-anchor="middle">CoreClient (httpx) → habla solo HTTP</text>
|
||
|
||
<!-- core -->
|
||
<rect class="dg-box violet" x="600" y="60" width="344" height="106" rx="13"/>
|
||
<text class="dg-t" x="772" y="92" text-anchor="middle">agentforge-core</text>
|
||
<text class="dg-s" x="772" y="112" text-anchor="middle">FastAPI · :8000 · "el cerebro"</text>
|
||
<text class="dg-m dim" x="772" y="132" text-anchor="middle">dominio · runtime · guardrails · persistencia</text>
|
||
<text class="dg-m dim" x="772" y="150" text-anchor="middle">/health · /agents · /executions · /policies · /violations</text>
|
||
|
||
<!-- arrow between -->
|
||
<line class="dg-edge accent" x1="356" y1="105" x2="600" y2="105" marker-end="url(#ah1)" marker-start="url(#ah1)"/>
|
||
<text class="dg-el bg" x="478" y="96" text-anchor="middle">HTTP / JSON</text>
|
||
<text class="dg-el bg" x="478" y="128" text-anchor="middle" fill="var(--muted)" style="font-weight:400">el dashboard es cliente del core</text>
|
||
|
||
<!-- walkthrough note -->
|
||
<rect class="dg-box ok dashed" x="600" y="190" width="344" height="40" rx="9"/>
|
||
<text class="dg-m" x="772" y="207" text-anchor="middle" fill="var(--ok)">docs/walkthrough.html</text>
|
||
<text class="dg-s" x="772" y="223" text-anchor="middle">este documento — HTML autocontenido, sin servidor</text>
|
||
|
||
<!-- core internals title -->
|
||
<text class="dg-frame-lbl" x="56" y="274" fill="var(--muted)">Dentro del core · todo enchufado una vez en api/deps.py</text>
|
||
|
||
<!-- 3 sublayers -->
|
||
<rect class="dg-box" x="56" y="292" width="278" height="98" rx="12"/>
|
||
<text class="dg-t sm" x="195" y="320" text-anchor="middle">capa LLM</text>
|
||
<text class="dg-s" x="195" y="340" text-anchor="middle">Strategy + factory</text>
|
||
<text class="dg-m dim" x="195" y="360" text-anchor="middle">mock · azure · openai</text>
|
||
<text class="dg-m dim" x="195" y="377" text-anchor="middle">¿qué dice el LLM?</text>
|
||
|
||
<rect class="dg-box" x="361" y="292" width="278" height="98" rx="12"/>
|
||
<text class="dg-t sm" x="500" y="320" text-anchor="middle">capa Guardrails</text>
|
||
<text class="dg-s" x="500" y="340" text-anchor="middle">Strategy + factory</text>
|
||
<text class="dg-m dim" x="500" y="360" text-anchor="middle">Composite( GuardrailsAI [, NeMo] )</text>
|
||
<text class="dg-m dim" x="500" y="377" text-anchor="middle">¿pasa los filtros?</text>
|
||
|
||
<rect class="dg-box" x="666" y="292" width="278" height="98" rx="12"/>
|
||
<text class="dg-t sm" x="805" y="320" text-anchor="middle">runtime LangGraph</text>
|
||
<text class="dg-s" x="805" y="340" text-anchor="middle">grafo + checkpointer</text>
|
||
<text class="dg-m dim" x="805" y="360" text-anchor="middle">orchestrator: invoke · resume · snapshot</text>
|
||
<text class="dg-m dim" x="805" y="377" text-anchor="middle">la ejecución, paso a paso</text>
|
||
|
||
<!-- core -> sublayers -->
|
||
<path class="dg-edge violet" d="M 700 166 C 600 230, 320 240, 220 292" fill="none" marker-end="url(#ah1)"/>
|
||
<path class="dg-edge violet" d="M 760 166 C 700 230, 540 245, 500 292" fill="none" marker-end="url(#ah1)"/>
|
||
<path class="dg-edge violet" d="M 820 166 C 850 230, 850 245, 805 292" fill="none" marker-end="url(#ah1)"/>
|
||
|
||
<!-- persistence -->
|
||
<rect class="dg-box soft" x="56" y="436" width="888" height="86" rx="13"/>
|
||
<text class="dg-t sm" x="500" y="463" text-anchor="middle">Persistencia — la herramienta adecuada para cada cosa</text>
|
||
<text class="dg-m" x="500" y="486" text-anchor="middle">YAML (definiciones) · JSON (índice trace_id→agente) · JSONL append-only (logs de auditoría) · SQLite (checkpoints HITL)</text>
|
||
<text class="dg-m dim" x="500" y="505" text-anchor="middle">agents/*.yaml · policies/*.yaml · data/execution_index.json · data/executions.jsonl · data/violations.jsonl · data/checkpoints.sqlite</text>
|
||
|
||
<line class="dg-edge thin" x1="195" y1="390" x2="195" y2="436" marker-end="url(#ah1)"/>
|
||
<line class="dg-edge thin" x1="500" y1="390" x2="500" y2="436" marker-end="url(#ah1)"/>
|
||
<line class="dg-edge thin" x1="805" y1="390" x2="805" y2="436" marker-end="url(#ah1)"/>
|
||
</svg>
|
||
<figcaption><b>Figura 1.</b> Los dos servicios y, dentro del core, las tres capas intercambiables (LLM, Guardrails, Runtime) más la persistencia. Cada flecha es "depende de / fluye hacia". Este documento (verde) es un HTML autocontenido — se consulta como fichero, no requiere servidor.</figcaption>
|
||
</figure>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="capas">
|
||
<h2><span class="kicker">03</span> Las capas del core, de fuera hacia dentro</h2>
|
||
<p>Una petición HTTP atraviesa estas capas en orden. Cada una solo conoce a la de dentro; <strong>nada de dentro conoce a las de fuera</strong> (eso es lo que mantiene el sistema desacoplado y testeable).</p>
|
||
|
||
<div class="flow v">
|
||
<div class="step"><div class="st">① HTTP request</div><div class="sd">POST /agents/{name}/invoke · {input: "…"}</div></div>
|
||
<div class="arr">→</div>
|
||
<div class="step"><div class="st">② TraceIdMiddleware</div><div class="sd">api/middlewares.py · genera/lee X-Trace-Id, lo bind-ea a structlog</div></div>
|
||
<div class="arr">→</div>
|
||
<div class="step"><div class="st">③ Router</div><div class="sd">api/agents.py · api/executions.py · api/policies.py · api/violations.py</div></div>
|
||
<div class="arr">→</div>
|
||
<div class="step"><div class="st">④ Dependencias (DI)</div><div class="sd">api/deps.py · @lru_cache: Registry, PolicyStore, LLMProvider, GuardrailEngine, Orchestrator (singletons)</div></div>
|
||
<div class="arr">→</div>
|
||
<div class="step"><div class="st">⑤ AgentOrchestrator</div><div class="sd">runtime/orchestrator.py · única puerta al runtime: invoke / resume / snapshot</div></div>
|
||
<div class="arr">→</div>
|
||
<div class="step"><div class="st">⑥ Grafo LangGraph</div><div class="sd">runtime/graph.py · cablea nodos + aristas condicionales, compila con checkpointer</div></div>
|
||
<div class="arr">→</div>
|
||
<div class="step"><div class="st">⑦ Nodos</div><div class="sd">runtime/nodes.py · validate_input → llm_reason → validate_output → propose_actions → approve_gate → finalize</div></div>
|
||
<div class="arr">→</div>
|
||
<div class="step"><div class="st">⑧ Adentro: llm/ · guardrails/ · domain/</div><div class="sd">¿qué dice el LLM? · ¿pasa los filtros? · ¿qué forma tienen los datos?</div></div>
|
||
<div class="arr">→</div>
|
||
<div class="step"><div class="st">⑨ Persistencia</div><div class="sd">checkpointer (SQLite) + api/persistence.py (JSONL) + registry (YAML/JSON)</div></div>
|
||
</div>
|
||
|
||
<div class="callout tip">
|
||
<div class="ct">🔒 Regla de oro</div>
|
||
<p>Solo <code>api/</code> importa de <code>runtime.orchestrator</code> (y <code>deps.py</code> lo construye). El resto de <code>api/</code> no toca LangGraph; <code>runtime/</code> no toca <code>api/</code>. Solo los <code>factory.py</code> y <code>main.py</code> conocen <code>Settings</code>. <code>domain/</code> no importa nada del proyecto.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="modulos">
|
||
<h2><span class="kicker">04</span> El grafo de módulos (quién depende de quién)</h2>
|
||
<p>El código del core vive bajo <code>core/src/agentforge_core/</code>. Es un <strong>DAG</strong>: las capas de abajo no importan nada de las de arriba. El "nivel" es la profundidad topológica. Lee de abajo hacia arriba: el vocabulario primero, la composición de la app al final.</p>
|
||
<div class="bands">
|
||
<div class="band">
|
||
<div class="lvl"><b>6</b><span>app</span></div>
|
||
<div class="mods">
|
||
<span class="mod core">main</span>
|
||
<span class="desc"><code>create_app()</code>: instancia FastAPI, añade <code>TraceIdMiddleware</code>, expone <code>/health</code>, monta los 5 routers. Construye <code>Settings()</code> para el logging.</span>
|
||
</div>
|
||
</div>
|
||
<div class="band">
|
||
<div class="lvl"><b>5</b><span>HTTP</span></div>
|
||
<div class="mods">
|
||
<span class="mod core">api.deps</span><span class="mod core">api.agents</span><span class="mod core">api.executions</span><span class="mod core">api.policies</span><span class="mod core">api.violations</span>
|
||
<span class="desc"><code>deps.py</code> construye y cachea los 5 objetos del dominio (<code>@lru_cache</code>); los routers solo los <em>piden</em> por parámetro.</span>
|
||
</div>
|
||
</div>
|
||
<div class="band">
|
||
<div class="lvl"><b>4</b><span>runtime</span></div>
|
||
<div class="mods">
|
||
<span class="mod core">runtime.orchestrator</span>
|
||
<span class="desc">Compone el grafo + el checkpointer; traduce el <code>StateSnapshot</code> de LangGraph a un <code>AgentExecution</code>. Único punto de entrada al runtime.</span>
|
||
</div>
|
||
</div>
|
||
<div class="band">
|
||
<div class="lvl"><b>3</b></div>
|
||
<div class="mods">
|
||
<span class="mod core">guardrails.factory</span><span class="mod core">runtime.nodes</span><span class="mod core">runtime.graph</span>
|
||
<span class="desc">Las funciones-nodo (parametrizadas con engine/policy/provider/agent) y el cableado del grafo; la factory que ensambla el <code>CompositeGuardrailEngine</code>.</span>
|
||
</div>
|
||
</div>
|
||
<div class="band">
|
||
<div class="lvl"><b>2</b></div>
|
||
<div class="mods">
|
||
<span class="mod core">llm.factory</span><span class="mod core">registry.factory</span><span class="mod core">guardrails.guardrails_ai</span><span class="mod core">guardrails.composite</span><span class="mod core">guardrails.nemo</span>
|
||
<span class="desc">Las factories de LLM y registry; los engines concretos de guardrails. <code>composite</code> no conoce a sus sub-engines (solo el <code>Protocol</code>).</span>
|
||
</div>
|
||
</div>
|
||
<div class="band">
|
||
<div class="lvl"><b>1</b></div>
|
||
<div class="mods">
|
||
<span class="mod core">domain.execution</span><span class="mod core">llm.mock</span><span class="mod core">llm.azure</span><span class="mod core">llm.openai</span><span class="mod core">registry.repository</span><span class="mod core">registry.policy_store</span><span class="mod core">guardrails.validators</span><span class="mod core">guardrails.base</span><span class="mod core">api.persistence</span><span class="mod core">api.middlewares</span>
|
||
<span class="desc">Implementaciones e interfaces concretas que dependen solo del nivel 0.</span>
|
||
</div>
|
||
</div>
|
||
<div class="band">
|
||
<div class="lvl"><b>0</b><span>vocab</span></div>
|
||
<div class="mods">
|
||
<span class="mod core">config</span><span class="mod core">observability.logging</span><span class="mod core">domain.agent</span><span class="mod core">domain.policy</span><span class="mod core">domain.guardrail</span><span class="mod core">registry.versioning</span><span class="mod core">llm.base</span><span class="mod core">runtime.state</span><span class="mod core">runtime.checkpointer</span>
|
||
<span class="desc">No importan <em>nada</em> del proyecto. <code>domain/</code> es el idioma común; todo lo demás depende de él.</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p class="muted">El paquete <code>dashboard/</code> no aparece aquí: no comparte código con el core, solo lo llama por HTTP.</p>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="patron">
|
||
<h2><span class="kicker">05</span> El patrón que se repite: <code>Protocol</code> + <code>factory</code> + <code>Settings</code></h2>
|
||
<p>Tres veces (LLM, guardrails, registry) verás la misma estructura. Permite <strong>cambiar el comportamiento sin tocar el código</strong>: pones <code>LLM_PROVIDER=azure</code> en <code>.env</code> y el factory te da el provider de Azure; pones <code>mock</code> y tienes un demo determinista sin claves. Es el principio de <em>"configuración antes que código"</em>.</p>
|
||
|
||
<figure class="diagram">
|
||
<svg viewBox="0 0 1000 360" role="img" aria-label="El patrón Strategy + Factory + Settings">
|
||
<defs><marker id="ah2" markerWidth="10" markerHeight="10" refX="7.5" refY="4" orient="auto-start-reverse"><path d="M0,0 L8.5,4 L0,8 Z" fill="context-stroke"/></marker></defs>
|
||
<!-- generic -->
|
||
<text class="dg-frame-lbl" x="30" y="28" fill="var(--muted)">La forma genérica</text>
|
||
<rect class="dg-box accent" x="30" y="40" width="190" height="46" rx="9"/>
|
||
<text class="dg-t sm" x="125" y="60" text-anchor="middle">base.py</text>
|
||
<text class="dg-s" x="125" y="76" text-anchor="middle">un Protocol (la interfaz)</text>
|
||
|
||
<rect class="dg-box" x="30" y="120" width="120" height="42" rx="8"/><text class="dg-m" x="90" y="146" text-anchor="middle">impl_a</text>
|
||
<rect class="dg-box" x="160" y="120" width="120" height="42" rx="8"/><text class="dg-m" x="220" y="146" text-anchor="middle">impl_b</text>
|
||
<rect class="dg-box" x="290" y="120" width="120" height="42" rx="8"/><text class="dg-m" x="350" y="146" text-anchor="middle">impl_c</text>
|
||
|
||
<rect class="dg-box violet" x="120" y="210" width="200" height="46" rx="9"/>
|
||
<text class="dg-t sm" x="220" y="230" text-anchor="middle">factory.py</text>
|
||
<text class="dg-s" x="220" y="246" text-anchor="middle">build_X(settings) → X</text>
|
||
|
||
<rect class="dg-box soft dashed" x="30" y="290" width="380" height="40" rx="9"/>
|
||
<text class="dg-m" x="220" y="315" text-anchor="middle">Settings (pydantic-settings ← .env) — el único que sabe de env vars</text>
|
||
|
||
<line class="dg-edge thin" x1="125" y1="86" x2="90" y2="120" marker-end="url(#ah2)"/>
|
||
<line class="dg-edge thin" x1="125" y1="86" x2="220" y2="120" marker-end="url(#ah2)"/>
|
||
<line class="dg-edge thin" x1="125" y1="86" x2="350" y2="120" marker-end="url(#ah2)"/>
|
||
<line class="dg-edge violet thin" x1="220" y1="162" x2="220" y2="210" marker-end="url(#ah2)"/>
|
||
<line class="dg-edge thin dashed" x1="220" y1="290" x2="220" y2="256" marker-end="url(#ah2)"/>
|
||
<text class="dg-el bg" x="285" y="186" text-anchor="middle">elige y monta uno</text>
|
||
|
||
<!-- divider -->
|
||
<line class="dg-divider" x1="470" y1="20" x2="470" y2="340"/>
|
||
|
||
<!-- 3 instances -->
|
||
<text class="dg-frame-lbl" x="510" y="28" fill="var(--muted)">Las tres instancias reales</text>
|
||
<rect class="dg-box accent" x="510" y="44" width="450" height="78" rx="10"/>
|
||
<text class="dg-t sm" x="525" y="66">LLM — llm/base.py: LLMProvider</text>
|
||
<text class="dg-m" x="525" y="86">MockProvider · AzureOpenAIProvider · OpenAIProvider</text>
|
||
<text class="dg-s" x="525" y="104">build_llm_provider(settings) → match settings.llm_provider · usado por el nodo llm_reason</text>
|
||
|
||
<rect class="dg-box violet" x="510" y="134" width="450" height="78" rx="10"/>
|
||
<text class="dg-t sm" x="525" y="156">Guardrails — guardrails/base.py: GuardrailEngine</text>
|
||
<text class="dg-m" x="525" y="176">GuardrailsAIEngine · NeMoGuardrailsEngine (opcional) · vía CompositeGuardrailEngine</text>
|
||
<text class="dg-s" x="525" y="194">build_guardrail_engine(settings) → NeMo solo si GUARDRAILS_NEMO_ENABLED</text>
|
||
|
||
<rect class="dg-box" x="510" y="224" width="450" height="78" rx="10"/>
|
||
<text class="dg-t sm" x="525" y="246">Registry — registry/repository.py · registry/policy_store.py</text>
|
||
<text class="dg-m" x="525" y="266">FileSystemAgentRegistry · FileSystemPolicyStore (hoy ficheros; mañana ¿BD?)</text>
|
||
<text class="dg-s" x="525" y="284">build_agent_registry(settings) · build_policy_store(settings)</text>
|
||
</svg>
|
||
<figcaption><b>Figura 2.</b> Izquierda: la forma genérica (interfaz <code>Protocol</code> → N implementaciones → un <code>factory</code> que elige según <code>Settings</code>). Derecha: las tres instancias concretas. Todo se enchufa <strong>una sola vez</strong> al arrancar, en <code>api/deps.py</code> (los <code>@lru_cache</code>).</figcaption>
|
||
</figure>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="dominio">
|
||
<h2><span class="kicker">06</span> El dominio — el vocabulario del sistema</h2>
|
||
<p>Modelos Pydantic puros bajo <code>domain/</code>. <strong>No dependen de nada del proyecto</strong>; todo lo demás depende de ellos. Si quieres entender el sistema rápido, empieza leyendo <code>domain/execution.py</code>: te dice exactamente qué información se produce y se guarda.</p>
|
||
|
||
<figure class="diagram">
|
||
<svg viewBox="0 0 1080 600" role="img" aria-label="Modelo de datos del dominio">
|
||
<defs><marker id="ah3" markerWidth="10" markerHeight="10" refX="7.5" refY="4" orient="auto-start-reverse"><path d="M0,0 L8.5,4 L0,8 Z" fill="context-stroke"/></marker></defs>
|
||
|
||
<!-- AgentDefinition -->
|
||
<rect class="dg-box accent" x="24" y="40" width="300" height="190" rx="11"/>
|
||
<text class="dg-t sm" x="40" y="64" fill="var(--accent)">AgentDefinition · agent.py</text>
|
||
<line x1="36" y1="74" x2="312" y2="74" stroke="var(--border)"/>
|
||
<text class="dg-m" x="40" y="94">name · version · owner · purpose</text>
|
||
<text class="dg-m" x="40" y="113">state: draft | active | deprecated</text>
|
||
<text class="dg-m" x="40" y="132">guardrails: list[str] → nombres de política</text>
|
||
<text class="dg-m" x="40" y="151">llm: LLMConfig</text>
|
||
<text class="dg-m" x="40" y="170">system_prompt: str</text>
|
||
<text class="dg-m" x="40" y="189">output_schema: dict (JSON Schema)</text>
|
||
<text class="dg-m" x="40" y="208">risk_threshold_for_hitl: int (1..5)</text>
|
||
<text class="dg-m dim" x="40" y="225">updated_at</text>
|
||
|
||
<!-- LLMConfig -->
|
||
<rect class="dg-box" x="24" y="252" width="146" height="92" rx="10"/>
|
||
<text class="dg-t sm" x="32" y="274">LLMConfig</text>
|
||
<text class="dg-m dim" x="32" y="293">provider</text><text class="dg-m dim" x="32" y="309">model</text>
|
||
<text class="dg-m dim" x="32" y="325">temperature</text><text class="dg-m dim" x="32" y="341">max_tokens</text>
|
||
|
||
<!-- AgentVersionMeta -->
|
||
<rect class="dg-box" x="180" y="252" width="144" height="92" rx="10"/>
|
||
<text class="dg-t sm" x="188" y="274">AgentVersionMeta</text>
|
||
<text class="dg-m dim" x="188" y="293">id · hash</text><text class="dg-m dim" x="188" y="309">author</text>
|
||
<text class="dg-m dim" x="188" y="325">message</text><text class="dg-m dim" x="188" y="341">created_at</text>
|
||
|
||
<line class="dg-edge thin" x1="84" y1="230" x2="84" y2="252" marker-end="url(#ah3)"/>
|
||
<line class="dg-edge thin dashed" x1="252" y1="230" x2="252" y2="252" marker-end="url(#ah3)"/>
|
||
<text class="dg-el bg" x="290" y="244" text-anchor="middle" style="font-weight:400">index.yaml</text>
|
||
|
||
<!-- PolicyDefinition -->
|
||
<rect class="dg-box violet" x="390" y="40" width="300" height="150" rx="11"/>
|
||
<text class="dg-t sm" x="406" y="64" fill="var(--accent-2)">PolicyDefinition · policy.py</text>
|
||
<line x1="402" y1="74" x2="678" y2="74" stroke="var(--border)"/>
|
||
<text class="dg-m" x="406" y="94">name · version · description</text>
|
||
<text class="dg-m" x="406" y="113">input_validators: list[PolicyValidator]</text>
|
||
<text class="dg-m" x="406" y="132">output_validators: list[PolicyValidator]</text>
|
||
<text class="dg-m" x="406" y="151">on_validator_error:</text>
|
||
<text class="dg-m" x="406" y="170"> fail_open | fail_closed (def: fail_closed)</text>
|
||
|
||
<rect class="dg-box" x="390" y="212" width="146" height="60" rx="10"/>
|
||
<text class="dg-t sm" x="398" y="234">PolicyValidator</text>
|
||
<text class="dg-m dim" x="398" y="253">type: str</text><text class="dg-m dim" x="398" y="267">config: dict</text>
|
||
|
||
<rect class="dg-box" x="546" y="212" width="144" height="60" rx="10"/>
|
||
<text class="dg-t sm" x="554" y="234">PolicyVersionMeta</text>
|
||
<text class="dg-m dim" x="554" y="253">id · hash · author</text><text class="dg-m dim" x="554" y="267">message · created_at</text>
|
||
|
||
<line class="dg-edge violet thin" x1="463" y1="190" x2="463" y2="212" marker-end="url(#ah3)"/>
|
||
<path class="dg-edge accent thin dashed" d="M 324 130 C 350 130, 360 110, 390 110" fill="none" marker-end="url(#ah3)"/>
|
||
<text class="dg-el bg" x="357" y="100" text-anchor="middle" style="font-weight:400">por nombre</text>
|
||
|
||
<!-- AgentExecution -->
|
||
<rect class="dg-box" x="756" y="40" width="300" height="296" rx="11"/>
|
||
<text class="dg-t sm" x="772" y="64">AgentExecution · execution.py</text>
|
||
<text class="dg-s" x="772" y="80">el "expediente" de una invocación · va a JSONL al terminar</text>
|
||
<line x1="768" y1="88" x2="1044" y2="88" stroke="var(--border)"/>
|
||
<text class="dg-m" x="772" y="108">trace_id: UUID · agent_name · agent_version</text>
|
||
<text class="dg-m" x="772" y="127">status: running | awaiting_approval |</text>
|
||
<text class="dg-m" x="772" y="143"> blocked_by_guardrail | completed | failed</text>
|
||
<text class="dg-m" x="772" y="162">started_at · finished_at</text>
|
||
<text class="dg-m" x="772" y="181">decision_path: list[DecisionStep]</text>
|
||
<text class="dg-m" x="772" y="200">violations: list[GuardrailViolation]</text>
|
||
<text class="dg-m" x="772" y="219">proposed_actions: list[ProposedAction]</text>
|
||
<text class="dg-m" x="772" y="238">needs_human_for: list[ProposedAction] | None</text>
|
||
<text class="dg-m" x="772" y="257">final_output: dict | None · error: str | None</text>
|
||
|
||
<rect class="dg-box" x="756" y="276" width="143" height="60" rx="10"/>
|
||
<text class="dg-t sm" x="764" y="297" style="font-size:11px">DecisionStep</text>
|
||
<text class="dg-m dim" x="764" y="314" style="font-size:10px">step · timestamp</text><text class="dg-m dim" x="764" y="328" style="font-size:10px">duration_ms · detail</text>
|
||
|
||
<rect class="dg-box" x="905" y="276" width="151" height="60" rx="10"/>
|
||
<text class="dg-t sm" x="913" y="297" style="font-size:11px">ProposedAction</text>
|
||
<text class="dg-m dim" x="913" y="314" style="font-size:10px">id · action · target</text><text class="dg-m dim" x="913" y="328" style="font-size:10px">risk_score(1-5) · rollback_plan · requires_approval</text>
|
||
|
||
<!-- GuardrailViolation -->
|
||
<rect class="dg-box danger" x="390" y="320" width="330" height="116" rx="11"/>
|
||
<text class="dg-t sm" x="406" y="344" fill="var(--danger)">GuardrailViolation · guardrail.py</text>
|
||
<line x1="402" y1="354" x2="708" y2="354" stroke="var(--border)"/>
|
||
<text class="dg-m" x="406" y="374">trace_id: UUID · timestamp</text>
|
||
<text class="dg-m" x="406" y="393">stage: input | output · validator: str</text>
|
||
<text class="dg-m" x="406" y="412">severity: info | warning | block</text>
|
||
<text class="dg-m" x="406" y="431">message: str · blocked: bool</text>
|
||
|
||
<path class="dg-edge thin" d="M 906 257 C 900 280, 880 290, 850 276" fill="none" marker-end="url(#ah3)"/>
|
||
<path class="dg-edge thin" d="M 940 257 C 950 280, 960 290, 970 276" fill="none" marker-end="url(#ah3)"/>
|
||
<path class="dg-edge danger thin" d="M 756 215 C 700 250, 650 290, 640 320" fill="none" marker-end="url(#ah3)"/>
|
||
<text class="dg-el bg" x="690" y="290" text-anchor="middle" style="font-weight:400">acumula</text>
|
||
|
||
<!-- AgentExecutionSummary -->
|
||
<rect class="dg-box soft" x="756" y="356" width="300" height="40" rx="9"/>
|
||
<text class="dg-m" x="906" y="380" text-anchor="middle">AgentExecutionSummary — versión ligera para listados</text>
|
||
<line class="dg-edge thin dashed" x1="906" y1="336" x2="906" y2="356" marker-end="url(#ah3)"/>
|
||
</svg>
|
||
<figcaption><b>Figura 3.</b> Los modelos del dominio. <code>AgentDefinition</code> referencia políticas por nombre; el <code>AgentRegistry</code> resuelve la versión activa vía <code>index.yaml</code>. <code>AgentExecution</code> es el expediente que se acumula durante la ejecución y se persiste al terminar; sus violaciones se escriben además, una a una, en <code>violations.jsonl</code>.</figcaption>
|
||
</figure>
|
||
|
||
<div class="tbl-wrap">
|
||
<table>
|
||
<thead><tr><th>Fichero</th><th>Qué define</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>domain/agent.py</code></td><td><code>AgentDefinition</code> (prompt, modelo, <code>output_schema</code>, <code>guardrails</code>, <code>risk_threshold_for_hitl</code>…), <code>LLMConfig</code>, <code>AgentVersionMeta</code>.</td></tr>
|
||
<tr><td><code>domain/policy.py</code></td><td><code>PolicyDefinition</code> (listas de <code>PolicyValidator</code> de entrada y salida, <code>on_validator_error</code>), <code>PolicyVersionMeta</code>.</td></tr>
|
||
<tr><td><code>domain/guardrail.py</code></td><td><code>GuardrailViolation</code> (trace_id, stage <code>input</code>/<code>output</code>, validator, severity, message, blocked).</td></tr>
|
||
<tr><td><code>domain/execution.py</code></td><td><code>AgentExecution</code> (status, <code>decision_path</code>, <code>violations</code>, <code>proposed_actions</code>, <code>needs_human_for</code>, <code>final_output</code>, <code>error</code>), <code>ProposedAction</code>, <code>DecisionStep</code>, <code>AgentExecutionSummary</code>.</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="guardrails">
|
||
<h2><span class="kicker">07</span> Guardrails — el motor de validación</h2>
|
||
<p>Aquí está el corazón del "gobierno". La estructura sigue otra vez <strong>Protocol + implementaciones + composite + factory</strong>. Una política lista qué validar; el motor lo aplica. Por defecto, <strong>fail-closed</strong>: si un validador peta, cuenta como bloqueo (seguridad antes que disponibilidad).</p>
|
||
|
||
<figure class="diagram">
|
||
<svg viewBox="0 0 1020 470" role="img" aria-label="Composición del motor de guardrails">
|
||
<defs><marker id="ah4" markerWidth="10" markerHeight="10" refX="7.5" refY="4" orient="auto-start-reverse"><path d="M0,0 L8.5,4 L0,8 Z" fill="context-stroke"/></marker></defs>
|
||
|
||
<!-- policy in -->
|
||
<rect class="dg-box violet" x="24" y="180" width="210" height="110" rx="11"/>
|
||
<text class="dg-t sm" x="129" y="204" text-anchor="middle">PolicyDefinition</text>
|
||
<text class="dg-m dim" x="129" y="224" text-anchor="middle">policies/default/v1.yaml</text>
|
||
<line x1="40" y1="232" x2="218" y2="232" stroke="var(--border)"/>
|
||
<text class="dg-m" x="40" y="250">input_validators[…]</text>
|
||
<text class="dg-m" x="40" y="268">output_validators[…]</text>
|
||
<text class="dg-m" x="40" y="286">on_validator_error: fail_closed</text>
|
||
|
||
<!-- composite -->
|
||
<rect class="dg-box accent" x="300" y="40" width="320" height="74" rx="11"/>
|
||
<text class="dg-t sm" x="460" y="64" text-anchor="middle">CompositeGuardrailEngine</text>
|
||
<text class="dg-s" x="460" y="82" text-anchor="middle">corre N sub-engines en paralelo (asyncio.gather)</text>
|
||
<text class="dg-s" x="460" y="100" text-anchor="middle">y aplana las listas de violaciones</text>
|
||
|
||
<!-- sub-engines -->
|
||
<rect class="dg-box" x="300" y="160" width="320" height="64" rx="10"/>
|
||
<text class="dg-t sm" x="460" y="184" text-anchor="middle">GuardrailsAIEngine</text>
|
||
<text class="dg-s" x="460" y="202" text-anchor="middle">el real: aplica los validadores de la política</text>
|
||
<text class="dg-s" x="460" y="218" text-anchor="middle">integra Presidio para PII (con fallback a regex)</text>
|
||
|
||
<rect class="dg-box soft dashed" x="300" y="252" width="320" height="56" rx="10"/>
|
||
<text class="dg-t sm" x="460" y="276" text-anchor="middle" fill="var(--muted)">NeMoGuardrailsEngine</text>
|
||
<text class="dg-s" x="460" y="294" text-anchor="middle">stub · solo si GUARDRAILS_NEMO_ENABLED=true</text>
|
||
|
||
<!-- registries -->
|
||
<rect class="dg-box" x="690" y="150" width="306" height="86" rx="10"/>
|
||
<text class="dg-t sm" x="704" y="172">INPUT_VALIDATORS (type → fn)</text>
|
||
<text class="dg-m dim" x="704" y="190">detect_pii · prompt_injection</text>
|
||
<text class="dg-m dim" x="704" y="206">toxic_language · forbidden_topics</text>
|
||
<text class="dg-m dim" x="704" y="224" fill="var(--muted)">payload = str (texto del usuario)</text>
|
||
|
||
<rect class="dg-box" x="690" y="254" width="306" height="100" rx="10"/>
|
||
<text class="dg-t sm" x="704" y="276">OUTPUT_VALIDATORS (type → fn)</text>
|
||
<text class="dg-m dim" x="704" y="294">schema_match · pii_leakage</text>
|
||
<text class="dg-m dim" x="704" y="310">forbidden_action_keywords</text>
|
||
<text class="dg-m dim" x="704" y="326">telco_safety_rules</text>
|
||
<text class="dg-m dim" x="704" y="344" fill="var(--muted)">payload = dict (JSON del LLM)</text>
|
||
|
||
<!-- edges -->
|
||
<path class="dg-edge violet thin" d="M 234 220 C 270 200, 280 120, 300 88" fill="none" marker-end="url(#ah4)"/>
|
||
<path class="dg-edge violet thin" d="M 234 250 C 270 250, 280 200, 300 192" fill="none" marker-end="url(#ah4)"/>
|
||
<text class="dg-el bg" x="270" y="150" text-anchor="middle" style="font-weight:400">qué validar</text>
|
||
<line class="dg-edge accent thin" x1="460" y1="114" x2="460" y2="160" marker-end="url(#ah4)"/>
|
||
<line class="dg-edge thin dashed" x1="460" y1="114" x2="460" y2="252" marker-end="url(#ah4)" opacity="0.7"/>
|
||
<path class="dg-edge thin" d="M 620 184 C 660 184, 670 190, 690 190" fill="none" marker-end="url(#ah4)"/>
|
||
<path class="dg-edge thin" d="M 620 200 C 660 220, 670 290, 690 300" fill="none" marker-end="url(#ah4)"/>
|
||
<text class="dg-el bg" x="655" y="170" text-anchor="middle" style="font-weight:400">busca cada type</text>
|
||
|
||
<!-- output: violations -->
|
||
<rect class="dg-box danger dashed" x="300" y="360" width="320" height="48" rx="10"/>
|
||
<text class="dg-m" x="460" y="384" text-anchor="middle" fill="var(--danger)">→ list[GuardrailViolation] · cualquier blocked=True frena el grafo</text>
|
||
<path class="dg-edge thin" d="M 460 224 L 460 252" marker-end="url(#ah4)" opacity="0"/>
|
||
<line class="dg-edge danger thin" x1="460" y1="308" x2="460" y2="360" marker-end="url(#ah4)"/>
|
||
<text class="dg-el bg" x="500" y="338" text-anchor="middle" style="font-weight:400">agrega</text>
|
||
|
||
<text class="dg-frame-lbl" x="24" y="438" fill="var(--muted)">fail_closed: si una fn validadora lanza → se añade una violación severity=block, blocked=True. type desconocido → log warning, no bloquea.</text>
|
||
</svg>
|
||
<figcaption><b>Figura 4.</b> El motor de guardrails. La política dice <em>qué</em> validar; <code>GuardrailsAIEngine</code> recorre esa lista, busca cada <code>type</code> en su registro de funciones de <code>validators.py</code>, las llama con <code>(payload, config, trace_id, kind)</code>, y junta las <code>GuardrailViolation</code> que devuelvan. <code>CompositeGuardrailEngine</code> corre los sub-engines en paralelo y agrega.</figcaption>
|
||
</figure>
|
||
|
||
<h3>Los validadores concretos (<code>guardrails/validators.py</code>)</h3>
|
||
<div class="tbl-wrap">
|
||
<table>
|
||
<thead><tr><th>Validador</th><th>Etapa</th><th>Qué comprueba</th><th>Severidad típica</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>detect_pii</code></td><td><span class="pill accent">input</span></td><td>PII vía Presidio (modelo spaCy <code>en_core_web_sm</code>) o, si no está, regex de EMAIL / PHONE / ES_NIF / IP. La política <code>default</code> pide solo recognizers de patrón fiables (<code>EMAIL_ADDRESS, ES_NIF, IP_ADDRESS, IBAN_CODE</code>) para evitar falsos positivos del NER.</td><td><span class="pill danger">block</span></td></tr>
|
||
<tr><td><code>prompt_injection</code></td><td><span class="pill accent">input</span></td><td>Heurísticas de patrones conocidos ("ignore previous instructions", "you are now", "reveal the system prompt"…).</td><td><span class="pill danger">block</span></td></tr>
|
||
<tr><td><code>toxic_language</code></td><td><span class="pill accent">input</span></td><td>Lista negra con umbral (MVP).</td><td><span class="pill warn">warning</span></td></tr>
|
||
<tr><td><code>forbidden_topics</code></td><td><span class="pill accent">input</span></td><td>Coincidencia de substring con temas prohibidos (instrucciones de explotación, credenciales, código malicioso).</td><td><span class="pill danger">block</span></td></tr>
|
||
<tr><td><code>schema_match</code></td><td><span class="pill violet">output</span></td><td>Valida el JSON del LLM contra un JSON Schema (severity, root_cause_hypothesis, proposed_actions con id/action/target/risk_score/rollback_plan/requires_approval).</td><td><span class="pill danger">block</span></td></tr>
|
||
<tr><td><code>pii_leakage</code></td><td><span class="pill violet">output</span></td><td>Re-aplica <code>detect_pii</code> sobre la salida serializada.</td><td><span class="pill danger">block</span></td></tr>
|
||
<tr><td><code>forbidden_action_keywords</code></td><td><span class="pill violet">output</span></td><td>Que las acciones propuestas no contengan comandos peligrosos (<code>DROP TABLE</code>, <code>rm -rf</code>, <code>shutdown -h now</code>, <code>delete production</code>, <code>format c:</code>).</td><td><span class="pill danger">block</span></td></tr>
|
||
<tr><td><code>telco_safety_rules</code></td><td><span class="pill violet">output</span></td><td>Reglas declarativas: nunca acción sobre <code>prod</code> sin rollback; nunca acción masiva (<code>all/todos/*</code>) sin <code>canary</code> en el plan.</td><td><span class="pill danger">block</span></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<p>Así se conecta una política con un validador (extracto de <code>policies/default/versions/v1.yaml</code>):</p>
|
||
<div class="code">
|
||
<div class="hd"><span class="dot"></span><span class="fn">policies/default/versions/v1.yaml</span><span class="lang">yaml</span></div>
|
||
<pre><span class="y">name</span>: default
|
||
<span class="y">version</span>: v1
|
||
<span class="y">input_validators</span>:
|
||
- <span class="y">type</span>: detect_pii
|
||
<span class="y">config</span>: { <span class="y">entities</span>: [EMAIL_ADDRESS, ES_NIF, IP_ADDRESS, IBAN_CODE], <span class="y">severity_on_match</span>: block }
|
||
- <span class="y">type</span>: prompt_injection
|
||
<span class="y">config</span>: { <span class="y">severity_on_match</span>: block }
|
||
- <span class="y">type</span>: toxic_language
|
||
<span class="y">config</span>: { <span class="y">threshold</span>: <span class="n">0.7</span>, <span class="y">severity_on_match</span>: warning }
|
||
- <span class="y">type</span>: forbidden_topics
|
||
<span class="y">config</span>: { <span class="y">topics</span>: [<span class="s">"instrucciones de explotación"</span>, <span class="s">"credenciales"</span>, <span class="s">"código malicioso"</span>], <span class="y">severity_on_match</span>: block }
|
||
<span class="y">output_validators</span>:
|
||
- <span class="y">type</span>: schema_match <span class="c"># valida el JSON del LLM contra un JSON Schema</span>
|
||
<span class="y">config</span>: { <span class="y">severity_on_mismatch</span>: block, <span class="y">schema</span>: { … } }
|
||
- <span class="y">type</span>: pii_leakage
|
||
- <span class="y">type</span>: forbidden_action_keywords
|
||
<span class="y">config</span>: { <span class="y">keywords</span>: [<span class="d">"DROP TABLE"</span>, <span class="d">"rm -rf"</span>, <span class="d">"shutdown -h now"</span>, <span class="d">"delete production"</span>, <span class="d">"format c:"</span>] }
|
||
- <span class="y">type</span>: telco_safety_rules
|
||
<span class="y">config</span>: { <span class="y">rules</span>: [never_propose_action_targeting_production_without_rollback, never_propose_mass_action_without_canary] }
|
||
<span class="y">on_validator_error</span>: fail_closed <span class="c"># si un validador lanza una excepción → cuenta como bloqueo</span></pre>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="runtime">
|
||
<h2><span class="kicker">08</span> El grafo de ejecución (LangGraph)</h2>
|
||
<p>Una ejecución del agente se modela como un <strong>grafo dirigido de estados con checkpoints</strong>. No es "llama al LLM y ya": es un flujo con aristas condicionales y una pausa real para Human-in-the-Loop. Cada nodo añade un <code>DecisionStep</code> con su duración al <code>decision_path</code>.</p>
|
||
|
||
<div class="legend">
|
||
<span class="li"><span class="sw box"></span> nodo del grafo</span>
|
||
<span class="li"><span class="sw warn"></span> pausa (estado persistido)</span>
|
||
<span class="li"><span class="sw danger"></span> salida temprana (status terminal)</span>
|
||
<span class="li">— camino normal · - - camino condicional</span>
|
||
</div>
|
||
|
||
<figure class="diagram">
|
||
<svg viewBox="0 0 1080 740" role="img" aria-label="El grafo de ejecución LangGraph">
|
||
<defs><marker id="ah5" markerWidth="10" markerHeight="10" refX="7.5" refY="4" orient="auto-start-reverse"><path d="M0,0 L8.5,4 L0,8 Z" fill="context-stroke"/></marker></defs>
|
||
|
||
<!-- START -->
|
||
<ellipse class="dg-box accent" cx="200" cy="40" rx="44" ry="20"/>
|
||
<text class="dg-t sm" x="200" y="45" text-anchor="middle">START</text>
|
||
|
||
<!-- validate_input -->
|
||
<rect class="dg-box" x="120" y="92" width="160" height="56" rx="10"/>
|
||
<text class="dg-t sm" x="200" y="116" text-anchor="middle">validate_input</text>
|
||
<text class="dg-s" x="200" y="134" text-anchor="middle">engine.validate_input(...)</text>
|
||
|
||
<!-- llm_reason -->
|
||
<rect class="dg-box" x="120" y="200" width="160" height="56" rx="10"/>
|
||
<text class="dg-t sm" x="200" y="224" text-anchor="middle">llm_reason</text>
|
||
<text class="dg-s" x="200" y="242" text-anchor="middle">provider.complete([sys, user])</text>
|
||
|
||
<!-- validate_output -->
|
||
<rect class="dg-box" x="120" y="308" width="160" height="56" rx="10"/>
|
||
<text class="dg-t sm" x="200" y="332" text-anchor="middle">validate_output</text>
|
||
<text class="dg-s" x="200" y="350" text-anchor="middle">json.loads + engine.validate_output</text>
|
||
|
||
<!-- propose_actions -->
|
||
<rect class="dg-box" x="120" y="416" width="160" height="56" rx="10"/>
|
||
<text class="dg-t sm" x="200" y="440" text-anchor="middle">propose_actions</text>
|
||
<text class="dg-s" x="200" y="458" text-anchor="middle">extrae proposed_actions[]</text>
|
||
|
||
<!-- approve_gate -->
|
||
<rect class="dg-box accent" x="100" y="524" width="200" height="64" rx="10"/>
|
||
<text class="dg-t sm" x="200" y="548" text-anchor="middle">approve_gate</text>
|
||
<text class="dg-s" x="200" y="566" text-anchor="middle">¿alguna acción con risk_score ≥ umbral</text>
|
||
<text class="dg-s" x="200" y="581" text-anchor="middle">o requires_approval = True?</text>
|
||
|
||
<!-- finalize -->
|
||
<rect class="dg-box" x="120" y="640" width="160" height="56" rx="10"/>
|
||
<text class="dg-t sm" x="200" y="664" text-anchor="middle">finalize</text>
|
||
<text class="dg-s" x="200" y="682" text-anchor="middle">filtra a las acciones aprobadas</text>
|
||
|
||
<!-- END (bottom) -->
|
||
<ellipse class="dg-box" cx="200" cy="722" rx="38" ry="18"/>
|
||
<text class="dg-t sm" x="200" y="727" text-anchor="middle">END</text>
|
||
|
||
<!-- main spine edges -->
|
||
<line class="dg-edge accent" x1="200" y1="60" x2="200" y2="92" marker-end="url(#ah5)"/>
|
||
<line class="dg-edge" x1="200" y1="148" x2="200" y2="200" marker-end="url(#ah5)"/><text class="dg-el bg" x="216" y="178">ok</text>
|
||
<line class="dg-edge" x1="200" y1="256" x2="200" y2="308" marker-end="url(#ah5)"/><text class="dg-el bg" x="216" y="286">ok</text>
|
||
<line class="dg-edge" x1="200" y1="364" x2="200" y2="416" marker-end="url(#ah5)"/><text class="dg-el bg" x="216" y="394">ok</text>
|
||
<line class="dg-edge" x1="200" y1="472" x2="200" y2="524" marker-end="url(#ah5)"/>
|
||
<line class="dg-edge" x1="200" y1="640" x2="200" y2="623" opacity="0"/>
|
||
<path class="dg-edge" d="M 160 588 C 130 605, 130 620, 160 640" fill="none" marker-end="url(#ah5)"/><text class="dg-el bg" x="118" y="616">no hay riesgo</text>
|
||
<line class="dg-edge" x1="200" y1="696" x2="200" y2="704" marker-end="url(#ah5)"/>
|
||
|
||
<!-- early-exit edges to a right-side END column -->
|
||
<ellipse class="dg-box danger" cx="600" cy="120" rx="40" ry="18"/><text class="dg-t sm" x="600" y="125" text-anchor="middle">END</text>
|
||
<path class="dg-edge danger dashed" d="M 280 120 C 420 120, 470 120, 560 120" fill="none" marker-end="url(#ah5)"/>
|
||
<text class="dg-el bg" x="420" y="110" text-anchor="middle">bloqueada — PII, injection, topic… → status = blocked_by_guardrail</text>
|
||
|
||
<ellipse class="dg-box danger" cx="600" cy="228" rx="40" ry="18"/><text class="dg-t sm" x="600" y="233" text-anchor="middle">END</text>
|
||
<path class="dg-edge danger dashed" d="M 280 228 C 420 228, 470 228, 560 228" fill="none" marker-end="url(#ah5)"/>
|
||
<text class="dg-el bg" x="420" y="218" text-anchor="middle">LLM no disponible / error → status = failed (llm_unavailable)</text>
|
||
|
||
<ellipse class="dg-box danger" cx="600" cy="336" rx="40" ry="18"/><text class="dg-t sm" x="600" y="341" text-anchor="middle">END</text>
|
||
<path class="dg-edge danger dashed" d="M 280 336 C 420 336, 470 336, 560 336" fill="none" marker-end="url(#ah5)"/>
|
||
<text class="dg-el bg" x="420" y="326" text-anchor="middle">salida no parsea / no cumple esquema / PII en la salida → blocked_by_guardrail | failed</text>
|
||
|
||
<!-- HITL branch -->
|
||
<rect class="dg-box warn" x="430" y="500" width="290" height="112" rx="12"/>
|
||
<text class="dg-t sm" x="575" y="524" text-anchor="middle" fill="var(--warn)">interrupt({ awaiting_actions: [...] })</text>
|
||
<text class="dg-s" x="575" y="544" text-anchor="middle">el grafo SE PAUSA aquí.</text>
|
||
<text class="dg-s" x="575" y="560" text-anchor="middle">LangGraph persiste el estado en</text>
|
||
<text class="dg-m" x="575" y="578" text-anchor="middle">data/checkpoints.sqlite</text>
|
||
<text class="dg-s" x="575" y="598" text-anchor="middle">status del snapshot = awaiting_approval</text>
|
||
|
||
<path class="dg-edge warn" d="M 300 552 C 360 530, 380 525, 430 540" fill="none" marker-end="url(#ah5)"/>
|
||
<text class="dg-el bg" x="365" y="522" text-anchor="middle">hay riesgo → sí</text>
|
||
|
||
<!-- resume back -->
|
||
<rect class="dg-box ok dashed" x="770" y="430" width="280" height="74" rx="11"/>
|
||
<text class="dg-t sm" x="910" y="454" text-anchor="middle" fill="var(--ok)">…más tarde (¡incluso tras reiniciar!)…</text>
|
||
<text class="dg-m" x="910" y="474" text-anchor="middle">resume(Command(resume=decision))</text>
|
||
<text class="dg-s" x="910" y="492" text-anchor="middle">POST /executions/{trace_id}/approve | /reject</text>
|
||
|
||
<path class="dg-edge ok dashed" d="M 720 540 C 800 540, 900 530, 910 504" fill="none" marker-end="url(#ah5)"/>
|
||
<path class="dg-edge ok dashed" d="M 870 430 C 700 380, 360 420, 250 524" fill="none" marker-end="url(#ah5)"/>
|
||
<text class="dg-el bg" x="560" y="404" text-anchor="middle">interrupt() devuelve la decisión → approve_gate continúa</text>
|
||
|
||
<!-- finalize branch: rejected -->
|
||
<ellipse class="dg-box danger" cx="600" cy="668" rx="40" ry="18"/><text class="dg-t sm" x="600" y="673" text-anchor="middle">END</text>
|
||
<path class="dg-edge danger dashed" d="M 280 668 C 420 668, 470 668, 560 668" fill="none" marker-end="url(#ah5)"/>
|
||
<text class="dg-el bg" x="420" y="690" text-anchor="middle">human_decision.rejected → status = failed (rejected_by_human)</text>
|
||
<text class="dg-el bg" x="200" y="618" text-anchor="middle">si no hubo HITL: todas las acciones</text>
|
||
</svg>
|
||
<figcaption><b>Figura 5.</b> El grafo. Columna izquierda: el camino feliz <code>START → validate_input → llm_reason → validate_output → propose_actions → approve_gate → finalize → END</code>. Salidas tempranas (rojo) cuando un guardrail bloquea o algo falla. <code>approve_gate</code> bifurca: si no hay riesgo, va directo a <code>finalize</code>; si lo hay, llama a <code>interrupt()</code> y <strong>el grafo se detiene</strong>, con el estado en SQLite, hasta que llega un <code>resume(decision)</code> — que puede ocurrir tras un reinicio del proceso.</figcaption>
|
||
</figure>
|
||
|
||
<h3>Los nodos (<code>runtime/nodes.py</code>) y el estado (<code>runtime/state.py</code>)</h3>
|
||
<div class="tbl-wrap">
|
||
<table>
|
||
<thead><tr><th>Nodo (factory)</th><th>Hace</th><th>Puede saltar a</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>validate_input</code><br><span class="muted">build_node_validate_input(engine, policy)</span></td><td>Corre los <code>input_validators</code> de la política sobre el texto del usuario; acumula violaciones.</td><td><code>END</code> si alguna <code>blocked</code> → <code>status = blocked_by_guardrail</code></td></tr>
|
||
<tr><td><code>llm_reason</code><br><span class="muted">build_node_llm_reason(provider, agent_def)</span></td><td><code>provider.complete([Message("system", system_prompt), Message("user", input)], temperature, max_tokens)</code>. Guarda <code>raw_llm_output</code> y un step con modelo/tokens/latencia.</td><td><code>END</code> si el provider lanza → <code>status = failed</code>, <code>error = llm_unavailable</code></td></tr>
|
||
<tr><td><code>validate_output</code><br><span class="muted">build_node_validate_output(engine, policy)</span></td><td><code>json.loads</code> del output; corre los <code>output_validators</code> (<code>schema_match</code>, <code>pii_leakage</code>, …).</td><td><code>END</code> si no parsea (<code>output_schema_mismatch</code>) o hay <code>blocked</code></td></tr>
|
||
<tr><td><code>propose_actions</code><br><span class="muted">build_node_propose_actions()</span></td><td>Extrae <code>parsed_output["proposed_actions"]</code> al estado.</td><td>siempre → <code>approve_gate</code></td></tr>
|
||
<tr><td><code>approve_gate</code><br><span class="muted">build_node_approve_gate(agent_def)</span></td><td>Filtra acciones <em>arriesgadas</em> (<code>risk_score ≥ risk_threshold_for_hitl</code> o <code>requires_approval</code>). Si las hay → <code>interrupt({"awaiting_actions": risky})</code> (pausa). Al reanudar, recibe la <code>decision</code> humana.</td><td>siempre → <code>finalize</code> (tras la pausa, si la hubo)</td></tr>
|
||
<tr><td><code>finalize</code><br><span class="muted">build_node_finalize()</span></td><td>Si <code>human_decision.rejected</code> → <code>status = failed</code>, <code>error = rejected_by_human</code>. Si no → <code>final_output = {**parsed, "approved_actions": <acciones aprobadas o todas si no hubo HITL>}</code>, <code>status = completed</code>.</td><td><code>END</code></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<p>El estado que fluye por el grafo es un <code>TypedDict</code>. <code>decision_path</code> usa un <em>reducer</em> (<code>Annotated[list, operator.add]</code>) para que cada nodo <strong>añada</strong> pasos en vez de sobrescribir:</p>
|
||
<div class="code">
|
||
<div class="hd"><span class="dot"></span><span class="fn">core/src/agentforge_core/runtime/state.py</span><span class="lang">python</span></div>
|
||
<pre><span class="k">class</span> <span class="y">AgentState</span>(TypedDict, total=<span class="k">False</span>):
|
||
trace_id: <span class="y">str</span>; agent_name: <span class="y">str</span>; agent_version: <span class="y">str</span>; user_input: <span class="y">str</span>
|
||
messages: <span class="y">list</span>[<span class="y">dict</span>]; raw_llm_output: <span class="y">str</span> | <span class="k">None</span>; parsed_output: <span class="y">dict</span> | <span class="k">None</span>
|
||
proposed_actions: <span class="y">list</span>[<span class="y">dict</span>]; violations: <span class="y">list</span>[<span class="y">dict</span>]
|
||
decision_path: Annotated[<span class="y">list</span>[<span class="y">dict</span>], operator.add] <span class="c"># ← reducer: cada nodo AÑADE pasos</span>
|
||
status: <span class="y">str</span>; error: <span class="y">str</span> | <span class="k">None</span>
|
||
human_decision: <span class="y">dict</span> | <span class="k">None</span>; final_output: <span class="y">dict</span> | <span class="k">None</span></pre>
|
||
</div>
|
||
|
||
<details class="dd">
|
||
<summary>Mirar dentro: el cableado del grafo y el nodo que pausa</summary>
|
||
<div class="body">
|
||
<p>El grafo se cablea con aristas fijas y <strong>condicionales</strong> (<code>runtime/graph.py</code>): tras <code>validate_input</code>, si <code>status == "blocked_by_guardrail"</code> → <code>END</code>; tras <code>llm_reason</code>, si <code>status == "failed"</code> → <code>END</code>; etc. <code>propose_actions → approve_gate → finalize → END</code> son aristas fijas.</p>
|
||
<div class="code">
|
||
<div class="hd"><span class="dot"></span><span class="fn">runtime/graph.py — aristas condicionales</span><span class="lang">python</span></div>
|
||
<pre>g.add_edge(START, <span class="s">"validate_input"</span>)
|
||
|
||
<span class="k">def</span> <span class="y">_after_validate_input</span>(state):
|
||
<span class="k">return</span> END <span class="k">if</span> state.get(<span class="s">"status"</span>) == <span class="s">"blocked_by_guardrail"</span> <span class="k">else</span> <span class="s">"llm_reason"</span>
|
||
g.add_conditional_edges(<span class="s">"validate_input"</span>, _after_validate_input, {END: END, <span class="s">"llm_reason"</span>: <span class="s">"llm_reason"</span>})
|
||
|
||
<span class="c"># … _after_llm → END si status=="failed" · _after_validate_output → END si status in {blocked, failed} …</span>
|
||
g.add_edge(<span class="s">"propose_actions"</span>, <span class="s">"approve_gate"</span>)
|
||
g.add_edge(<span class="s">"approve_gate"</span>, <span class="s">"finalize"</span>)
|
||
g.add_edge(<span class="s">"finalize"</span>, END)
|
||
<span class="k">return</span> g.compile(checkpointer=checkpointer)</pre>
|
||
</div>
|
||
<div class="code">
|
||
<div class="hd"><span class="dot"></span><span class="fn">runtime/nodes.py — build_node_approve_gate</span><span class="lang">python</span></div>
|
||
<pre><span class="k">def</span> <span class="y">build_node_approve_gate</span>(agent_def):
|
||
<span class="k">async def</span> <span class="y">approve_gate</span>(state):
|
||
actions = state.get(<span class="s">"proposed_actions"</span>, [])
|
||
risky = [a <span class="k">for</span> a <span class="k">in</span> actions
|
||
<span class="k">if</span> <span class="y">int</span>(a.get(<span class="s">"risk_score"</span>, <span class="n">1</span>)) >= agent_def.risk_threshold_for_hitl
|
||
<span class="k">or</span> <span class="y">bool</span>(a.get(<span class="s">"requires_approval"</span>))]
|
||
<span class="k">if not</span> risky:
|
||
<span class="k">return</span> {<span class="s">"decision_path"</span>: [_step(<span class="s">"approve_gate"</span>, started, hitl=<span class="k">False</span>)]}
|
||
decision = <span class="y">interrupt</span>({<span class="s">"awaiting_actions"</span>: risky}) <span class="c"># ← pausa; reanuda con Command(resume=decision)</span>
|
||
<span class="k">return</span> {<span class="s">"human_decision"</span>: decision,
|
||
<span class="s">"decision_path"</span>: [_step(<span class="s">"approve_gate"</span>, started, hitl=<span class="k">True</span>, resumed=<span class="k">True</span>)]}
|
||
<span class="k">return</span> approve_gate</pre>
|
||
</div>
|
||
<p>El <code>AgentOrchestrator</code> abre su <strong>propio</strong> <code>AsyncSqliteSaver</code> en cada operación (<code>invoke</code> / <code>resume</code> / <code>snapshot</code>) sobre <code>data_dir/checkpoints.sqlite</code> — por eso un <code>awaiting_approval</code> sobrevive a un reinicio: basta crear otro orchestrator apuntando al mismo <code>data_dir</code> y llamar a <code>resume</code>. El orchestrator traduce el <code>StateSnapshot</code> de LangGraph a un <code>AgentExecution</code>: si <code>state.next</code> (hay un nodo pendiente) y el <code>status</code> no es terminal ⇒ es el <code>interrupt()</code> ⇒ <code>status = "awaiting_approval"</code> y <code>needs_human_for</code> = las acciones arriesgadas.</p>
|
||
</div>
|
||
</details>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="viaje">
|
||
<h2><span class="kicker">09</span> El viaje de UNA petición, de principio a fin</h2>
|
||
<p>Esta es la sección que conviene leer despacio: aquí se ve cómo encaja todo. Seguimos <code>POST /agents/incident_analyzer/invoke</code> con el escenario SIP — el que dispara HITL — y luego la aprobación.</p>
|
||
|
||
<figure class="diagram">
|
||
<svg viewBox="0 0 1120 802" role="img" aria-label="Diagrama de secuencia: el viaje de una petición">
|
||
<defs><marker id="ah6" markerWidth="9" markerHeight="9" refX="7" refY="4" orient="auto-start-reverse"><path d="M0,0 L8,4 L0,8 Z" fill="context-stroke"/></marker></defs>
|
||
|
||
<!-- actor headers -->
|
||
<rect class="dg-actor" x="18" y="18" width="144" height="42" rx="8"/><text class="dg-t sm" x="90" y="36" text-anchor="middle">Cliente</text><text class="dg-s" x="90" y="51" text-anchor="middle">dashboard / curl</text>
|
||
<rect class="dg-actor" x="218" y="18" width="148" height="42" rx="8"/><text class="dg-t sm" x="292" y="36" text-anchor="middle">router</text><text class="dg-s" x="292" y="51" text-anchor="middle">api/executions.py · +middleware</text>
|
||
<rect class="dg-actor" x="416" y="18" width="148" height="42" rx="8"/><text class="dg-t sm" x="490" y="36" text-anchor="middle">AgentOrchestrator</text><text class="dg-s" x="490" y="51" text-anchor="middle">runtime/orchestrator.py</text>
|
||
<rect class="dg-actor" x="612" y="18" width="140" height="42" rx="8"/><text class="dg-t sm" x="682" y="36" text-anchor="middle">Grafo LangGraph</text><text class="dg-s" x="682" y="51" text-anchor="middle">nodes + checkpointer</text>
|
||
<rect class="dg-actor" x="788" y="18" width="140" height="42" rx="8"/><text class="dg-t sm" x="858" y="36" text-anchor="middle">checkpoints</text><text class="dg-s" x="858" y="51" text-anchor="middle">data/checkpoints.sqlite</text>
|
||
<rect class="dg-actor" x="958" y="18" width="146" height="42" rx="8"/><text class="dg-t sm" x="1031" y="36" text-anchor="middle">logs / índice</text><text class="dg-s" x="1031" y="51" text-anchor="middle">executions.jsonl · index.json</text>
|
||
|
||
<!-- lifelines -->
|
||
<line class="dg-life" x1="90" y1="60" x2="90" y2="792"/>
|
||
<line class="dg-life" x1="292" y1="60" x2="292" y2="792"/>
|
||
<line class="dg-life" x1="490" y1="60" x2="490" y2="792"/>
|
||
<line class="dg-life" x1="682" y1="60" x2="682" y2="792"/>
|
||
<line class="dg-life" x1="858" y1="60" x2="858" y2="792"/>
|
||
<line class="dg-life" x1="1031" y1="60" x2="1031" y2="792"/>
|
||
|
||
<!-- ACT 1 label -->
|
||
<rect class="dg-box accent" x="18" y="72" width="220" height="22" rx="6"/><text class="dg-t sm" x="28" y="88" style="font-size:11.5px">ACTO 1 · invoke → awaiting_approval</text>
|
||
|
||
<!-- m1 -->
|
||
<line class="dg-edge accent" x1="90" y1="116" x2="292" y2="116" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="91" y="108" style="text-anchor:start">POST /agents/incident_analyzer/invoke { input }</text>
|
||
<text class="dg-el bg" x="91" y="128" style="text-anchor:start;font-weight:400;fill:var(--muted)">TraceIdMiddleware genera trace_id = UUID y lo bind-ea al log</text>
|
||
<!-- m2 self -->
|
||
<path class="dg-edge" d="M 292 150 h 30 v 18 h -30" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="330" y="156" style="text-anchor:start">registry.get_agent → AgentDefinition (v2 activa)</text>
|
||
<text class="dg-el bg" x="330" y="172" style="text-anchor:start;font-weight:400">policies.get_policy(agent.guardrails[0]) → PolicyDefinition</text>
|
||
<!-- m3 -->
|
||
<line class="dg-edge" x1="292" y1="196" x2="490" y2="196" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="293" y="188" style="text-anchor:start">orchestrator.invoke(agent_def, policy, user_input)</text>
|
||
<!-- m4 -->
|
||
<line class="dg-edge" x1="490" y1="222" x2="682" y2="222" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="491" y="214" style="text-anchor:start">graph.ainvoke(estado_inicial, config={thread_id: trace_id})</text>
|
||
<!-- m5 self -->
|
||
<path class="dg-edge" d="M 682 246 h 30 v 18 h -30" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="720" y="252" style="text-anchor:start">validate_input → llm_reason → validate_output → propose_actions</text>
|
||
<text class="dg-el bg" x="720" y="268" style="text-anchor:start;font-weight:400">0 violaciones · MockProvider ve "sip" → respuesta canónica</text>
|
||
<!-- m6 self -->
|
||
<path class="dg-edge warn" d="M 682 292 h 30 v 18 h -30" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="720" y="298" style="text-anchor:start;fill:var(--warn)">approve_gate: act-1 (risk 4, requires_approval) → interrupt()</text>
|
||
<text class="dg-el bg" x="720" y="314" style="text-anchor:start;font-weight:400">⇒ el grafo SE DETIENE aquí</text>
|
||
<!-- m7 -->
|
||
<line class="dg-edge warn" x1="682" y1="338" x2="858" y2="338" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="683" y="330" style="text-anchor:start;fill:var(--warn)">persiste el estado pausado</text>
|
||
<!-- m8 return -->
|
||
<line class="dg-edge dashed" x1="682" y1="364" x2="490" y2="364" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="488" y="356" style="text-anchor:end">aget_state → hay nodo pendiente</text>
|
||
<!-- m9 return -->
|
||
<line class="dg-edge dashed" x1="490" y1="390" x2="292" y2="390" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="488" y="382" style="text-anchor:end">AgentExecution(status=awaiting_approval, needs_human_for=[act-1])</text>
|
||
<!-- m10 -->
|
||
<line class="dg-edge" x1="292" y1="416" x2="1031" y2="416" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="293" y="408" style="text-anchor:start">execution_index.json[trace_id] = (incident_analyzer, v2) · (no es terminal → NO se escribe en executions.jsonl)</text>
|
||
<!-- m11 return -->
|
||
<line class="dg-edge accent dashed" x1="292" y1="442" x2="90" y2="442" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="290" y="434" style="text-anchor:end">200 { status: awaiting_approval, trace_id, decision_path, … }</text>
|
||
|
||
<!-- divider -->
|
||
<rect class="dg-band" x="14" y="460" width="1092" height="34" rx="6"/>
|
||
<line class="dg-divider" x1="14" y1="460" x2="1106" y2="460"/><line class="dg-divider" x1="14" y1="494" x2="1106" y2="494"/>
|
||
<text class="dg-s" x="560" y="481" text-anchor="middle" style="font-style:italic">· · · más tarde — incluso tras reiniciar agentforge-core: el estado pausado sigue en checkpoints.sqlite · · ·</text>
|
||
|
||
<!-- ACT 2 label -->
|
||
<rect class="dg-box ok" x="18" y="504" width="232" height="22" rx="6"/><text class="dg-t sm" x="28" y="520" style="font-size:11.5px;fill:var(--ok)">ACTO 2 · el humano decide → completed</text>
|
||
|
||
<!-- n1 -->
|
||
<line class="dg-edge accent" x1="90" y1="548" x2="292" y2="548" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="91" y="540" style="text-anchor:start">POST /executions/{trace_id}/approve { approved_action_ids: [act-1], comment }</text>
|
||
<!-- n2 -->
|
||
<line class="dg-edge" x1="292" y1="574" x2="1031" y2="574" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="293" y="566" style="text-anchor:start">_resolve: lee execution_index.json → reconstruye agent_def + policy</text>
|
||
<!-- n3 -->
|
||
<line class="dg-edge" x1="292" y1="600" x2="490" y2="600" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="293" y="592" style="text-anchor:start">_ensure_awaiting: snapshot → status == awaiting_approval ✓ (409 si no)</text>
|
||
<!-- n4 -->
|
||
<line class="dg-edge" x1="292" y1="626" x2="490" y2="626" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="293" y="618" style="text-anchor:start">orchestrator.resume(trace_id, decision={approved_action_ids:[act-1], rejected:false})</text>
|
||
<!-- n5 -->
|
||
<line class="dg-edge" x1="490" y1="652" x2="858" y2="652" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="491" y="644" style="text-anchor:start">reabre el checkpointer (mismo data_dir) — el estado pausado sigue ahí</text>
|
||
<!-- n6 -->
|
||
<line class="dg-edge" x1="490" y1="678" x2="682" y2="678" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="491" y="670" style="text-anchor:start">graph.ainvoke(Command(resume=decision), config={thread_id: trace_id})</text>
|
||
<!-- n7 self -->
|
||
<path class="dg-edge ok" d="M 682 702 h 30 v 18 h -30" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="720" y="708" style="text-anchor:start;fill:var(--ok)">approve_gate (interrupt() devuelve la decisión) → finalize</text>
|
||
<text class="dg-el bg" x="720" y="724" style="text-anchor:start;font-weight:400">final_output={…, approved_actions:[act-1]} · status = completed</text>
|
||
<!-- n8 return -->
|
||
<line class="dg-edge dashed" x1="682" y1="748" x2="490" y2="748" marker-end="url(#ah6)"/>
|
||
<line class="dg-edge dashed" x1="490" y1="748" x2="292" y2="748" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="488" y="740" style="text-anchor:end">AgentExecution(status=completed, final_output)</text>
|
||
<!-- n9 -->
|
||
<line class="dg-edge" x1="292" y1="772" x2="1031" y2="772" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="293" y="764" style="text-anchor:start">append → executions.jsonl · (status terminal)</text>
|
||
<!-- n10 return -->
|
||
<line class="dg-edge accent dashed" x1="292" y1="792" x2="90" y2="792" marker-end="url(#ah6)"/>
|
||
<text class="dg-el bg" x="290" y="786" style="text-anchor:end">200 { status: completed, final_output, … }</text>
|
||
</svg>
|
||
<figcaption><b>Figura 6.</b> Diagrama de secuencia. <b>Acto 1</b>: la petición entra, el middleware pone un <code>trace_id</code>, el router resuelve agente+política, el orchestrator lanza el grafo, éste recorre cuatro nodos y se pausa en <code>approve_gate</code> con un <code>interrupt()</code> — el estado va a SQLite, se anota el índice y se responde <code>awaiting_approval</code> (sin escribir aún en el log append-only). <b>Acto 2</b> (puede ser tras un reinicio): llega el <code>approve</code>, el router reconstruye la config desde el índice, el orchestrator reabre el checkpointer y reanuda con <code>Command(resume=...)</code>; el grafo termina, y como el status ya es terminal se escribe en <code>executions.jsonl</code>. Si en vez de <code>approve</code> llega <code>reject</code> → <code>finalize</code> ve <code>rejected: true</code> → <code>status = failed</code>, <code>error = rejected_by_human</code>.</figcaption>
|
||
</figure>
|
||
|
||
<p>El mismo recorrido, con <code>curl</code> (provider <code>mock</code>, sin claves):</p>
|
||
<div class="code">
|
||
<div class="hd"><span class="dot"></span><span class="fn">demo end-to-end con curl</span><span class="lang">bash</span></div>
|
||
<pre><span class="c"># 1) Invocar — el escenario SIP dispara HITL</span>
|
||
TRACE=$(curl -s localhost:8000/agents/incident_analyzer/invoke \
|
||
-H <span class="s">'content-type: application/json'</span> \
|
||
-d <span class="s">'{"input": "Caída de registros SIP tras desplegar la imagen 4.7.2 en CSCF aravaca-01"}'</span> \
|
||
| python -c <span class="s">'import sys,json; print(json.load(sys.stdin)["trace_id"])'</span>)
|
||
<span class="c"># → status: "awaiting_approval", needs_human_for: [act-1]</span>
|
||
|
||
<span class="c"># 2) Revisar la cola HITL</span>
|
||
curl -s localhost:8000/executions | python -m json.tool <span class="c"># incluye las awaiting_approval reconstruidas del índice + checkpointer</span>
|
||
|
||
<span class="c"># 3) Aprobar la acción segura act-1</span>
|
||
curl -s localhost:8000/executions/$TRACE/approve \
|
||
-H <span class="s">'content-type: application/json'</span> \
|
||
-d <span class="s">'{"approved_action_ids": ["act-1"], "comment": "rollback ok"}'</span>
|
||
<span class="c"># → status: "completed", final_output: { ..., "approved_actions": [act-1] } · ya escrito en executions.jsonl</span>
|
||
|
||
<span class="c"># (el dashboard hace exactamente esto desde las páginas "Ejecutar" y "Aprobaciones")</span></pre>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="versionado">
|
||
<h2><span class="kicker">10</span> Versionado tipo Git</h2>
|
||
<p>Un agente no es una fila en una BD: es una carpeta con un <code>index.yaml</code> (el "catálogo de commits") y <code>versions/v1.yaml</code>, <code>versions/v2.yaml</code>… (los "commits"). Cada versión tiene un <strong>hash SHA-256</strong> del contenido normalizado, y dos versiones son comparables con un <strong>diff unificado</strong> (<code>difflib</code>). Lo mismo para las políticas. Cambias un YAML → nueva versión; promueves cambiando <code>active_version</code>. Sin redeploy.</p>
|
||
|
||
<div class="cols">
|
||
<div>
|
||
<h4>El índice (<code>agents/incident_analyzer/index.yaml</code>)</h4>
|
||
<div class="code">
|
||
<div class="hd"><span class="dot"></span><span class="fn">index.yaml</span><span class="lang">yaml</span></div>
|
||
<pre><span class="y">name</span>: incident_analyzer
|
||
<span class="y">versions</span>:
|
||
- <span class="y">id</span>: v1
|
||
<span class="y">hash</span>: pending
|
||
<span class="y">author</span>: Juan
|
||
<span class="y">message</span>: Versión inicial; SIP/IMS y MOS básicos.
|
||
<span class="y">created_at</span>: 2026-04-12T10:00:00Z
|
||
- <span class="y">id</span>: v2
|
||
<span class="y">hash</span>: pending
|
||
<span class="y">author</span>: Juan
|
||
<span class="y">message</span>: Añade codec mismatch y refuerza prompts.
|
||
<span class="y">created_at</span>: 2026-05-01T12:00:00Z
|
||
<span class="y">active_version</span>: v2 <span class="c"># ← invoke usa esta si no pides otra</span></pre>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<h4>El "commit" activo (<code>versions/v2.yaml</code>)</h4>
|
||
<div class="code">
|
||
<div class="hd"><span class="dot"></span><span class="fn">versions/v2.yaml</span><span class="lang">yaml</span></div>
|
||
<pre><span class="y">name</span>: incident_analyzer
|
||
<span class="y">version</span>: v2
|
||
<span class="y">owner</span>: Juan
|
||
<span class="y">state</span>: active <span class="c"># draft | active | deprecated</span>
|
||
<span class="y">guardrails</span>: [default] <span class="c"># nombres de política</span>
|
||
<span class="y">llm</span>:
|
||
<span class="y">provider</span>: mock
|
||
<span class="y">model</span>: gpt-4o
|
||
<span class="y">temperature</span>: <span class="n">0.1</span>
|
||
<span class="y">max_tokens</span>: <span class="n">2000</span>
|
||
<span class="y">system_prompt</span>: |
|
||
Eres un analista senior de operaciones de plataforma de voz
|
||
virtualizada (IMS, CSCF, SBC, HSS). Devuelves SIEMPRE un JSON
|
||
con severity, root_cause_hypothesis y proposed_actions. …
|
||
<span class="y">output_schema</span>:
|
||
<span class="y">type</span>: object
|
||
<span class="y">required</span>: [severity, root_cause_hypothesis, proposed_actions]
|
||
…
|
||
<span class="y">risk_threshold_for_hitl</span>: <span class="n">4</span> <span class="c"># risk_score ≥ 4 (o requires_approval) → HITL</span></pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="callout tip">
|
||
<div class="ct">🔎 La "magia tipo Git"</div>
|
||
<p><code>registry/versioning.py</code> tiene <code>compute_hash(yaml_text)</code> (SHA-256 del YAML con espacios al final recortados) y <code>unified_diff(a, b, label_a, label_b)</code>. El endpoint <code>GET /agents/{name}/versions/{v_from}/diff/{v_to}</code> devuelve un <code>DiffResult</code> con el <code>unified_diff</code>; el dashboard lo pinta coloreado (<code>+</code> verde, <code>-</code> rojo, <code>@@</code> azul) en la página <em>Registro</em>. (En los <code>index.yaml</code> de ejemplo el <code>hash</code> es <code>"pending"</code> y nadie lo valida al cargar — es un MVP.)</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="persistencia">
|
||
<h2><span class="kicker">11</span> Persistencia: cuatro formas, cuatro razones</h2>
|
||
<p>AgentForge no usa una sola base de datos; usa la herramienta adecuada para cada cosa.</p>
|
||
|
||
<figure class="diagram">
|
||
<svg viewBox="0 0 1020 420" role="img" aria-label="Mapa de persistencia: quién escribe y lee qué">
|
||
<defs><marker id="ah7" markerWidth="9" markerHeight="9" refX="7" refY="4" orient="auto-start-reverse"><path d="M0,0 L8,4 L0,8 Z" fill="context-stroke"/></marker></defs>
|
||
|
||
<!-- core in the middle -->
|
||
<rect class="dg-box violet" x="396" y="170" width="228" height="80" rx="12"/>
|
||
<text class="dg-t sm" x="510" y="196" text-anchor="middle">agentforge-core</text>
|
||
<text class="dg-s" x="510" y="214" text-anchor="middle">registry · orchestrator</text>
|
||
<text class="dg-s" x="510" y="230" text-anchor="middle">api/persistence · runtime/checkpointer</text>
|
||
|
||
<!-- YAML (top-left) -->
|
||
<rect class="dg-box" x="40" y="40" width="280" height="76" rx="11"/>
|
||
<text class="dg-t sm" x="56" y="64">YAML — agents/*.yaml · policies/*.yaml</text>
|
||
<text class="dg-s" x="56" y="82">lo escribe un humano (legible, comentable)</text>
|
||
<text class="dg-s" x="56" y="98">lo cataloga la máquina (index.yaml + versiones)</text>
|
||
<path class="dg-edge dashed" d="M 320 95 C 360 110, 380 150, 410 175" fill="none" marker-end="url(#ah7)"/>
|
||
<text class="dg-el bg" x="358" y="138" text-anchor="middle" style="font-weight:400">lee · diff · hash</text>
|
||
|
||
<!-- SQLite (top-right) -->
|
||
<rect class="dg-box warn" x="700" y="40" width="280" height="76" rx="11"/>
|
||
<text class="dg-t sm" x="716" y="64">SQLite — data/checkpoints.sqlite</text>
|
||
<text class="dg-s" x="716" y="82">estado intermedio del grafo, incluida la pausa HITL</text>
|
||
<text class="dg-s" x="716" y="98">vía LangGraph AsyncSqliteSaver · permite reanudar</text>
|
||
<path class="dg-edge warn" d="M 700 95 C 660 110, 640 150, 612 175" fill="none" marker-end="url(#ah7)" marker-start="url(#ah7)"/>
|
||
<text class="dg-el bg" x="660" y="138" text-anchor="middle">escribe/lee en cada transición</text>
|
||
|
||
<!-- JSON index (bottom-left) -->
|
||
<rect class="dg-box" x="40" y="300" width="280" height="76" rx="11"/>
|
||
<text class="dg-t sm" x="56" y="324">JSON — data/execution_index.json</text>
|
||
<text class="dg-s" x="56" y="342">{ trace_id: { agent_name, version } }</text>
|
||
<text class="dg-s" x="56" y="358">para reconstruir la config al reanudar un HITL</text>
|
||
<path class="dg-edge" d="M 410 240 C 380 270, 360 290, 320 322" fill="none" marker-end="url(#ah7)" marker-start="url(#ah7)"/>
|
||
<text class="dg-el bg" x="360" y="282" text-anchor="middle">_record_execution en cada invoke · _resolve lo lee</text>
|
||
|
||
<!-- JSONL executions (bottom-mid) -->
|
||
<rect class="dg-box ok" x="370" y="318" width="280" height="76" rx="11"/>
|
||
<text class="dg-t sm" x="386" y="342">JSONL append-only — data/executions.jsonl</text>
|
||
<text class="dg-s" x="386" y="360">un AgentExecution por línea · inmutable, auditable</text>
|
||
<text class="dg-s" x="386" y="376">se escribe si el status es terminal (y siempre tras approve/reject)</text>
|
||
<line class="dg-edge ok" x1="510" y1="250" x2="510" y2="318" marker-end="url(#ah7)"/>
|
||
<text class="dg-el bg" x="528" y="288" text-anchor="middle">append_execution</text>
|
||
|
||
<!-- JSONL violations (bottom-right) -->
|
||
<rect class="dg-box danger" x="700" y="300" width="280" height="76" rx="11"/>
|
||
<text class="dg-t sm" x="716" y="324">JSONL append-only — data/violations.jsonl</text>
|
||
<text class="dg-s" x="716" y="342">una GuardrailViolation por línea</text>
|
||
<text class="dg-s" x="716" y="358">se escribe una por violación de la ejecución</text>
|
||
<path class="dg-edge danger" d="M 612 240 C 650 270, 670 290, 700 322" fill="none" marker-end="url(#ah7)"/>
|
||
<text class="dg-el bg" x="668" y="282" text-anchor="middle">append_violation</text>
|
||
|
||
<!-- read endpoints note -->
|
||
<text class="dg-frame-lbl" x="40" y="160" fill="var(--muted)">GET /executions lee executions.jsonl + reconstruye las awaiting_approval del índice + checkpointer. GET /violations lee violations.jsonl.</text>
|
||
</svg>
|
||
<figcaption><b>Figura 7.</b> Quién escribe y quién lee cada artefacto. Las flechas con doble cabeza indican lectura+escritura. El núcleo: definiciones en YAML (humano), índice en JSON (para reanudar), logs en JSONL (auditoría inmutable), estado intermedio en SQLite (lo que hace posible el HITL persistente).</figcaption>
|
||
</figure>
|
||
|
||
<div class="tbl-wrap">
|
||
<table>
|
||
<thead><tr><th>Qué</th><th>Cómo</th><th>Por qué así</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Definiciones de agentes y políticas</td><td><strong>YAML</strong> por versión + <code>index.yaml</code></td><td>Lo escribe un humano (legible, comentable); lo cataloga la máquina.</td></tr>
|
||
<tr><td>Identidad de versiones / comparación</td><td><strong>hash SHA-256</strong> del contenido + <code>difflib</code></td><td>Versiones inmutables identificables y comparables — "tipo Git".</td></tr>
|
||
<tr><td>Mapa <code>trace_id → agente/versión</code></td><td><strong>JSON</strong> (<code>execution_index.json</code>)</td><td><code>/approve</code> y <code>/reject</code> solo reciben el <code>trace_id</code>: hay que reconstruir qué config lo ejecutó, y debe sobrevivir a reinicios.</td></tr>
|
||
<tr><td>Log de ejecuciones terminales y de violaciones</td><td><strong>JSONL append-only</strong></td><td>Inmutable, auditable, trivial de "shipear" a un sistema de logs.</td></tr>
|
||
<tr><td>Estado intermedio del grafo (incl. pausas HITL)</td><td><strong>SQLite</strong> (<code>checkpoints.sqlite</code>, vía LangGraph)</td><td>Es lo que LangGraph espera; permite reanudar una ejecución pausada entre reinicios del proceso.</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="observabilidad">
|
||
<h2><span class="kicker">12</span> Trazabilidad: el hilo del <code>trace_id</code></h2>
|
||
<p>Cada petición lleva un <code>trace_id</code> (UUID) que recorre todo el sistema. <code>structlog</code> emite JSON, una línea por evento, con ese <code>trace_id</code> en el contexto. Y cada nodo del grafo deja un <code>DecisionStep</code> en el <code>decision_path</code> con su <code>duration_ms</code> — la "caja negra" de la ejecución.</p>
|
||
|
||
<div class="flow">
|
||
<div class="step"><div class="st">Request entra</div><div class="sd">¿trae X-Trace-Id? si no, uuid4()</div></div>
|
||
<div class="arr">→</div>
|
||
<div class="step"><div class="st">TraceIdMiddleware</div><div class="sd">bind_trace_id(...) en structlog contextvars</div></div>
|
||
<div class="arr">→</div>
|
||
<div class="step"><div class="st">Routers · Orchestrator</div><div class="sd">todos los logs llevan trace_id; thread_id del grafo = trace_id</div></div>
|
||
<div class="arr">→</div>
|
||
<div class="step"><div class="st">Nodos del grafo</div><div class="sd">cada uno añade un DecisionStep {step, timestamp, duration_ms, detail}</div></div>
|
||
<div class="arr">→</div>
|
||
<div class="step"><div class="st">Guardrails</div><div class="sd">cada GuardrailViolation lleva el trace_id</div></div>
|
||
<div class="arr">→</div>
|
||
<div class="step"><div class="st">Persistencia</div><div class="sd">executions.jsonl · violations.jsonl · execution_index.json</div></div>
|
||
<div class="arr">→</div>
|
||
<div class="step"><div class="st">Response sale</div><div class="sd">cabecera X-Trace-Id (= el mismo)</div></div>
|
||
</div>
|
||
<p class="muted">Detalles del logging: <code>observability/logging.py</code> configura <code>structlog</code> con <code>JSONRenderer</code>, <code>TimeStamper(iso)</code>, niveles, y <code>merge_contextvars</code>. <code>bind_trace_id</code> / <code>clear_trace_id</code> manejan el contexto por request.</p>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="estados">
|
||
<h2><span class="kicker">13</span> Estados de una ejecución</h2>
|
||
<p>El <code>status</code> de un <code>AgentExecution</code> es uno de cinco. Tres son terminales (<span class="pill ok">completed</span>, <span class="pill danger">failed</span>, <span class="pill danger">blocked_by_guardrail</span>); <span class="pill warn">awaiting_approval</span> es el único que persiste indefinidamente esperando a un humano.</p>
|
||
|
||
<figure class="diagram">
|
||
<svg viewBox="0 0 980 430" role="img" aria-label="Máquina de estados de una ejecución">
|
||
<defs><marker id="ah8" markerWidth="9" markerHeight="9" refX="7" refY="4" orient="auto-start-reverse"><path d="M0,0 L8,4 L0,8 Z" fill="context-stroke"/></marker></defs>
|
||
|
||
<circle cx="60" cy="200" r="9" fill="var(--text)"/>
|
||
<line class="dg-edge" x1="69" y1="200" x2="120" y2="200" marker-end="url(#ah8)"/>
|
||
|
||
<rect class="dg-box accent" x="120" y="172" width="150" height="56" rx="10"/>
|
||
<text class="dg-t sm" x="195" y="196" text-anchor="middle">running</text>
|
||
<text class="dg-s" x="195" y="214" text-anchor="middle">recorriendo el grafo</text>
|
||
|
||
<!-- to blocked -->
|
||
<rect class="dg-box danger" x="370" y="40" width="200" height="56" rx="10"/>
|
||
<text class="dg-t sm" x="470" y="64" text-anchor="middle">blocked_by_guardrail</text>
|
||
<text class="dg-s" x="470" y="82" text-anchor="middle">un validador con blocked=True</text>
|
||
<path class="dg-edge danger" d="M 230 172 C 260 110, 320 80, 370 70" fill="none" marker-end="url(#ah8)"/>
|
||
<text class="dg-el bg" x="300" y="118" text-anchor="middle">validate_input / validate_output</text>
|
||
|
||
<!-- to failed -->
|
||
<rect class="dg-box danger" x="370" y="344" width="200" height="56" rx="10"/>
|
||
<text class="dg-t sm" x="470" y="368" text-anchor="middle">failed</text>
|
||
<text class="dg-s" x="470" y="386" text-anchor="middle">llm_unavailable · output_schema_mismatch · internal_error · rejected_by_human</text>
|
||
<path class="dg-edge danger" d="M 230 228 C 260 300, 320 340, 370 360" fill="none" marker-end="url(#ah8)"/>
|
||
<text class="dg-el bg" x="295" y="305" text-anchor="middle">error del LLM / parseo / crash</text>
|
||
|
||
<!-- to awaiting -->
|
||
<rect class="dg-box warn" x="370" y="172" width="210" height="56" rx="10"/>
|
||
<text class="dg-t sm" x="475" y="196" text-anchor="middle">awaiting_approval</text>
|
||
<text class="dg-s" x="475" y="214" text-anchor="middle">pausado en interrupt() · estado en SQLite</text>
|
||
<line class="dg-edge warn" x1="270" y1="200" x2="370" y2="200" marker-end="url(#ah8)"/>
|
||
<text class="dg-el bg" x="320" y="192" text-anchor="middle">approve_gate: hay riesgo</text>
|
||
|
||
<!-- awaiting self-loop: survives restart -->
|
||
<path class="dg-edge warn dashed" d="M 560 178 C 600 150, 600 250, 560 222" fill="none" marker-end="url(#ah8)"/>
|
||
<text class="dg-el bg" x="620" y="200" style="text-anchor:start">sobrevive a reinicios</text>
|
||
|
||
<!-- to completed -->
|
||
<rect class="dg-box ok" x="730" y="100" width="200" height="56" rx="10"/>
|
||
<text class="dg-t sm" x="830" y="124" text-anchor="middle">completed</text>
|
||
<text class="dg-s" x="830" y="142" text-anchor="middle">final_output con approved_actions</text>
|
||
|
||
<!-- to failed from awaiting (reject) -->
|
||
<path class="dg-edge danger" d="M 540 228 C 580 300, 480 340, 470 344" fill="none" marker-end="url(#ah8)"/>
|
||
<text class="dg-el bg" x="585" y="270" style="text-anchor:start">/reject → rejected_by_human</text>
|
||
|
||
<!-- awaiting -> completed (approve) -->
|
||
<path class="dg-edge ok" d="M 580 188 C 660 160, 680 140, 730 132" fill="none" marker-end="url(#ah8)"/>
|
||
<text class="dg-el bg" x="660" y="150" text-anchor="middle">/approve</text>
|
||
|
||
<!-- running -> completed (no HITL) -->
|
||
<path class="dg-edge ok" d="M 270 188 C 420 140, 520 80, 680 80 C 740 80, 760 95, 760 100" fill="none" marker-end="url(#ah8)"/>
|
||
<text class="dg-el bg" x="520" y="74" text-anchor="middle">sin acciones arriesgadas → finalize directo</text>
|
||
|
||
<!-- terminal markers -->
|
||
<circle cx="470" cy="20" r="6" fill="none" stroke="var(--danger)" stroke-width="2"/><line x1="470" y1="26" x2="470" y2="40" class="dg-edge danger thin"/>
|
||
<circle cx="830" cy="180" r="7" fill="none" stroke="var(--ok)" stroke-width="2"/><circle cx="830" cy="180" r="3" fill="var(--ok)"/><line x1="830" y1="156" x2="830" y2="173" class="dg-edge ok thin" opacity="0"/>
|
||
</svg>
|
||
<figcaption><b>Figura 8.</b> Estados de un <code>AgentExecution</code>. <code>running</code> → puede acabar <em>directamente</em> en <code>completed</code> (si no hay acciones arriesgadas), o en <code>blocked_by_guardrail</code> / <code>failed</code>, o quedarse en <code>awaiting_approval</code> hasta que un humano hace <code>/approve</code> (→ <code>completed</code>) o <code>/reject</code> (→ <code>failed</code>). El estado <code>awaiting_approval</code> sobrevive a un reinicio del proceso. (Estados de un <em>agente</em>, en su YAML: <code>draft → active → deprecated</code> — concepto distinto.)</figcaption>
|
||
</figure>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="api">
|
||
<h2><span class="kicker">14</span> Referencia de la API REST</h2>
|
||
<p>El core expone una API limpia. Todos los endpoints pasan por <code>TraceIdMiddleware</code> (lee/crea <code>X-Trace-Id</code>, lo devuelve en la respuesta). Los <code>response_model</code> son los modelos del dominio. Es exactamente lo que llama el dashboard (vía <code>CoreClient</code>).</p>
|
||
<div class="tbl-wrap">
|
||
<table>
|
||
<thead><tr><th>Método · ruta</th><th>Qué hace</th><th>Errores</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><span class="pill ok">GET</span> <code>/health</code></td><td><code>{"status": "ok"}</code> — usado por los healthchecks de Docker.</td><td>—</td></tr>
|
||
<tr><td><span class="pill ok">GET</span> <code>/agents</code></td><td>Lista de <code>AgentDefinition</code> (la versión activa de cada agente).</td><td>—</td></tr>
|
||
<tr><td><span class="pill ok">GET</span> <code>/agents/{name}</code></td><td>El agente (versión activa).</td><td><span class="pill warn">404</span> no existe</td></tr>
|
||
<tr><td><span class="pill ok">GET</span> <code>/agents/{name}/versions</code></td><td>Lista de <code>AgentVersionMeta</code> (id, hash, author, message, created_at).</td><td><span class="pill warn">404</span></td></tr>
|
||
<tr><td><span class="pill ok">GET</span> <code>/agents/{name}/versions/{version}</code></td><td>El <code>AgentDefinition</code> de esa versión concreta.</td><td><span class="pill warn">404</span></td></tr>
|
||
<tr><td><span class="pill ok">GET</span> <code>/agents/{name}/versions/{v_from}/diff/{v_to}</code></td><td><code>DiffResult</code> con el <code>unified_diff</code> entre dos versiones.</td><td>—</td></tr>
|
||
<tr><td><span class="pill accent">POST</span> <code>/agents/{name}/invoke</code></td><td><strong>Lanza una ejecución.</strong> Body <code>{input: str, version?: str}</code>. Resuelve agente+política, llama a <code>orchestrator.invoke</code>, anota el índice; si el status es terminal escribe en <code>executions.jsonl</code>; escribe las violaciones en <code>violations.jsonl</code>. Devuelve un <code>AgentExecution</code>.</td><td><span class="pill warn">404</span> agente · <span class="pill warn">422</span> sin política</td></tr>
|
||
<tr><td><span class="pill ok">GET</span> <code>/executions</code></td><td>Lista de <code>AgentExecutionSummary</code>: las terminales del JSONL <em>más</em> las <code>awaiting_approval</code> reconstruidas desde <code>execution_index.json</code> + el checkpointer.</td><td>—</td></tr>
|
||
<tr><td><span class="pill ok">GET</span> <code>/executions/{trace_id}</code></td><td>El <code>AgentExecution</code> completo (vía <code>orchestrator.snapshot</code>).</td><td><span class="pill warn">404</span> · <span class="pill warn">500</span> si la config referida falta</td></tr>
|
||
<tr><td><span class="pill accent">POST</span> <code>/executions/{trace_id}/approve</code></td><td>Reanuda un HITL. Body <code>{approved_action_ids: list[str], comment?: str}</code>. <code>resume(decision={...rejected:false})</code> → <code>finalize</code> filtra a las aprobadas → <code>completed</code>. Escribe en <code>executions.jsonl</code>.</td><td><span class="pill warn">404</span> · <span class="pill warn">409</span> si no está en <code>awaiting_approval</code></td></tr>
|
||
<tr><td><span class="pill accent">POST</span> <code>/executions/{trace_id}/reject</code></td><td>Rechaza un HITL. Body <code>{reason: str}</code>. <code>resume(decision={...rejected:true})</code> → <code>status = failed</code>, <code>error = rejected_by_human</code>. Escribe en <code>executions.jsonl</code>.</td><td><span class="pill warn">404</span> · <span class="pill warn">409</span></td></tr>
|
||
<tr><td><span class="pill ok">GET</span> <code>/policies</code></td><td>Lista de <code>PolicyDefinition</code> (validadores de entrada/salida y su config).</td><td>—</td></tr>
|
||
<tr><td><span class="pill ok">GET</span> <code>/policies/{name}/versions</code></td><td>Lista de <code>PolicyVersionMeta</code>.</td><td><span class="pill warn">404</span></td></tr>
|
||
<tr><td><span class="pill ok">GET</span> <code>/violations?trace_id=&severity=</code></td><td>Lista de <code>GuardrailViolation</code> desde <code>violations.jsonl</code>, con filtros opcionales por <code>trace_id</code> y <code>severity</code> (<code>info|warning|block</code>).</td><td>—</td></tr>
|
||
<tr><td colspan="3" class="muted">Además, FastAPI sirve <code>/docs</code> (Swagger UI) y <code>/openapi.json</code> automáticamente. El walkthrough que estás leyendo no es un endpoint: es un fichero (<code>docs/walkthrough.html</code>) — ver §<a href="#endpoint">19</a>.</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p class="muted">Helpers internos de <code>executions.py</code> alrededor del índice (<code>data/execution_index.json</code>): <code>_record_execution</code> (en cada <code>invoke</code>), <code>_resolve</code> (reconstruye <code>agent_def</code>/<code>policy</code>; 404/500), <code>_ensure_awaiting</code> (404/409).</p>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="agente">
|
||
<h2><span class="kicker">15</span> El agente de ejemplo: <code>incident_analyzer</code></h2>
|
||
<p>El repo trae un agente de operaciones de telco y un proveedor LLM <strong>mock determinista</strong> para que el demo funcione out-of-the-box, sin API keys ni red. <code>MockProvider</code> mira el texto de entrada y elige una de tres respuestas canónicas buscando subcadenas (<code>"sip"</code>, <code>"mos"</code>, <code>"hss"</code>, en ese orden); si no encuentra ninguna, una respuesta genérica (o, en tests con texto aleatorio, un fallback por hash). Cada respuesta es un JSON con <code>severity</code>, <code>root_cause_hypothesis</code> y <code>proposed_actions</code>.</p>
|
||
|
||
<div class="tbl-wrap">
|
||
<table>
|
||
<thead><tr><th>Escenario (<code>examples/*.txt</code>)</th><th>Keyword</th><th>Respuesta del mock — acciones propuestas</th><th>¿HITL?</th></tr></thead>
|
||
<tbody>
|
||
<tr>
|
||
<td><code>01_sip_registration_drop</code><br><span class="muted">caída de registros SIP tras desplegar imagen en CSCF</span></td>
|
||
<td><code>"sip"</code></td>
|
||
<td><code>act-1</code> <code>rollback_image</code> en <code>cscf-cluster-aravaca-01</code> · <span class="pill warn">risk 4</span> · <code>requires_approval</code> | <code>act-2</code> <code>drain_traffic_to_standby</code> · <span class="pill">risk 3</span></td>
|
||
<td><span class="pill warn">sí</span> — <code>act-1</code> (risk 4 ≥ umbral 4 + requires_approval)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>02_mos_degradation_pool_sbc</code><br><span class="muted">degradación de MOS en pool SBC en pico horario</span></td>
|
||
<td><code>"mos"</code></td>
|
||
<td><code>act-1</code> <code>scale_out_sbc_pool</code> en <code>sbc-pool-borde-norte</code> · <span class="pill">risk 2</span> · no requiere aprobación</td>
|
||
<td><span class="pill ok">no</span> — completa directo</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>03_hss_capacity_active_active</code><br><span class="muted">saturación replicada en HSS active-active</span></td>
|
||
<td><code>"hss"</code></td>
|
||
<td><code>act-1</code> <code>isolate_replication_link</code> en <code>hss-pair-madrid</code> · <span class="pill danger">risk 5</span> · <code>requires_approval</code></td>
|
||
<td><span class="pill warn">sí</span> — <code>act-1</code> (risk 5)</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<figure class="diagram">
|
||
<svg viewBox="0 0 1000 230" role="img" aria-label="Flujo del escenario SIP a través del mock">
|
||
<defs><marker id="ah9" markerWidth="9" markerHeight="9" refX="7" refY="4" orient="auto-start-reverse"><path d="M0,0 L8,4 L0,8 Z" fill="context-stroke"/></marker></defs>
|
||
<rect class="dg-box" x="20" y="80" width="180" height="64" rx="10"/><text class="dg-t sm" x="110" y="104" text-anchor="middle">input del incidente</text><text class="dg-s" x="110" y="122" text-anchor="middle">"…caída de registros SIP…"</text>
|
||
<rect class="dg-box accent" x="240" y="80" width="160" height="64" rx="10"/><text class="dg-t sm" x="320" y="104" text-anchor="middle">MockProvider</text><text class="dg-s" x="320" y="122" text-anchor="middle">¿contiene "sip"? → sí</text>
|
||
<rect class="dg-box" x="440" y="80" width="200" height="64" rx="10"/><text class="dg-t sm" x="540" y="100" text-anchor="middle">respuesta canónica SIP</text><text class="dg-s" x="540" y="118" text-anchor="middle">JSON: severity=high, hypothesis,</text><text class="dg-s" x="540" y="134" text-anchor="middle">proposed_actions=[act-1, act-2]</text>
|
||
<rect class="dg-box" x="680" y="42" width="160" height="50" rx="10"/><text class="dg-t sm" x="760" y="64" text-anchor="middle">propose_actions</text><text class="dg-s" x="760" y="80" text-anchor="middle">[act-1 risk4, act-2 risk3]</text>
|
||
<rect class="dg-box warn" x="680" y="120" width="300" height="64" rx="10"/><text class="dg-t sm" x="830" y="142" text-anchor="middle" fill="var(--warn)">approve_gate</text><text class="dg-s" x="830" y="160" text-anchor="middle">act-1.risk_score(4) ≥ risk_threshold_for_hitl(4)</text><text class="dg-s" x="830" y="176" text-anchor="middle">→ interrupt() → awaiting_approval</text>
|
||
<line class="dg-edge" x1="200" y1="112" x2="240" y2="112" marker-end="url(#ah9)"/>
|
||
<line class="dg-edge accent" x1="400" y1="112" x2="440" y2="112" marker-end="url(#ah9)"/>
|
||
<path class="dg-edge" d="M 640 100 C 660 90, 665 75, 680 67" fill="none" marker-end="url(#ah9)"/>
|
||
<path class="dg-edge warn" d="M 760 92 C 780 100, 800 110, 820 120" fill="none" marker-end="url(#ah9)"/>
|
||
</svg>
|
||
<figcaption><b>Figura 9.</b> El escenario SIP de punta a punta a través del mock. La palabra <code>"sip"</code> en el input selecciona la respuesta canónica; <code>act-1</code> tiene <code>risk_score = 4</code>, igual al umbral del agente (<code>risk_threshold_for_hitl = 4</code>) y además <code>requires_approval = true</code>, así que <code>approve_gate</code> pausa la ejecución.</figcaption>
|
||
</figure>
|
||
|
||
<p>Lo que <em>no</em> es código: <code>agents/incident_analyzer/</code> (el YAML del agente + <code>examples/*.txt</code>), <code>policies/default/</code> (la política), y <code>data/</code> (estado runtime gitignored: <code>checkpoints.sqlite</code>, <code>executions.jsonl</code>, <code>violations.jsonl</code>, <code>execution_index.json</code> — se crea sola). Para usar Azure OpenAI o OpenAI reales: edita <code>.env</code> (<code>LLM_PROVIDER=azure|openai</code> + las claves). Los providers reales tienen retry exponencial 1s/2s/4s.</p>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="dashboard">
|
||
<h2><span class="kicker">16</span> El dashboard (Streamlit)</h2>
|
||
<p>Una consola visual con cinco páginas. <strong>No contiene lógica de negocio</strong>: <code>client.py</code> (<code>CoreClient</code>, httpx síncrono con 2 retries) envuelve todos los endpoints del core y mapea 404/409/422 a <code>{"api_error": …}</code> para que las páginas puedan distinguir "la API rechazó la petición" de "ejecución sin error". <code>app.py</code> es la raíz: sidebar de branding + health-check del core. <strong>Nadie del core depende del dashboard.</strong></p>
|
||
<div class="tbl-wrap">
|
||
<table>
|
||
<thead><tr><th>Página</th><th>Qué muestra</th><th>Endpoints que usa</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>🏛️ <strong>Registro</strong><br><code>pages/1_…_Registro.py</code></td><td>Catálogo de agentes: detalle (prompt, esquema, LLM, guardrails), tabla de versiones, <strong>diff coloreado v1↔v2</strong>.</td><td><code>/agents</code>, <code>/agents/{n}</code>, <code>/agents/{n}/versions</code>, <code>/agents/{n}/versions/{a}/diff/{b}</code></td></tr>
|
||
<tr><td>▶️ <strong>Ejecutar</strong><br><code>pages/2_…_Ejecutar.py</code></td><td>Lanza un agente: botones con los escenarios pregrabados, textarea, <em>invoke</em>, y render del status + output + violaciones + <em>timeline</em> del <code>decision_path</code>. Avisa si quedó en <code>awaiting_approval</code>.</td><td><code>/agents</code>, <code>/agents/{n}/invoke</code></td></tr>
|
||
<tr><td>🤝 <strong>Aprobaciones</strong><br><code>pages/3_…_Aprobaciones.py</code></td><td>La cola de HITL: lista las ejecuciones <code>awaiting_approval</code>, muestra cada acción propuesta (risk_score coloreado, target, rollback_plan) con un checkbox, y aprueba el subconjunto elegido o rechaza con motivo.</td><td><code>/executions</code> (filtra), <code>/executions/{t}</code>, <code>/executions/{t}/approve</code>, <code>/executions/{t}/reject</code></td></tr>
|
||
<tr><td>📜 <strong>Historial</strong><br><code>pages/4_…_Historial.py</code></td><td>Pestaña de ejecuciones (tabla + detalle por <code>trace_id</code>) y pestaña de violaciones (filtrable por severidad).</td><td><code>/executions</code>, <code>/executions/{t}</code>, <code>/violations</code></td></tr>
|
||
<tr><td>📐 <strong>Politicas</strong><br><code>pages/5_…_Politicas.py</code></td><td>Inventario de políticas: validadores de entrada/salida (cada uno expandible con su config) y versiones.</td><td><code>/policies</code>, <code>/policies/{n}/versions</code></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p class="muted">Componentes reutilizables de UI: <code>components/diff_view</code> (pinta <code>+</code>/<code>-</code>/<code>@@</code>), <code>components/trace_view</code> (el timeline del <code>decision_path</code>), <code>components/violation_view</code> (badges de severidad). El dashboard no tiene tests unitarios (mal coste/beneficio para Streamlit); su verificación es la checklist manual de <code>docs/manual_qa.md</code>. Demo guiada en 3 pasos: <strong>Registro</strong> → comparar v1 vs v2 · <strong>Ejecutar</strong> → escenario SIP → pausa en HITL · <strong>Aprobaciones</strong> → aprobar las seguras → ejecución completa.</p>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="arranque">
|
||
<h2><span class="kicker">17</span> Arranque y configuración</h2>
|
||
<p>Out-of-the-box, sin claves: <code>cp .env.example .env</code> y <code>docker compose up</code> → dashboard en <a href="http://localhost:8501">localhost:8501</a> y API en <code>localhost:8000</code>. (Este documento se consulta abriendo <code>docs/walkthrough.html</code> en el navegador — no requiere servidor.)</p>
|
||
<div class="cols">
|
||
<div>
|
||
<h4>El ciclo de vida del proceso core</h4>
|
||
<ol>
|
||
<li>Importar <code>agentforge_core.main</code> ejecuta <code>app = create_app()</code>: <code>Settings()</code> → <code>configure_logging(level)</code> → <code>FastAPI(...)</code> → <code>add_middleware(TraceIdMiddleware)</code> → registra <code>GET /health</code> → importa los routers → <code>include_router</code> (agents, executions ×2, policies, violations).</li>
|
||
<li>Las dependencias (<code>get_registry</code>, <code>get_policy_store</code>, <code>get_llm_provider</code>, <code>get_guardrail_engine</code>, <code>get_orchestrator</code>) <strong>no</strong> se construyen aún; se construyen y cachean en la <strong>primera request</strong> que las inyecta.</li>
|
||
<li>Cada request: <code>TraceIdMiddleware.dispatch</code> → router → resuelve <code>Depends(...)</code> (que puede disparar la construcción perezosa) → handler → respuesta con <code>X-Trace-Id</code>.</li>
|
||
</ol>
|
||
<p class="muted">En contenedores: <code>uvicorn agentforge_core.main:app --host 0.0.0.0 --port 8000</code>; <code>HEALTHCHECK</code> → <code>curl /health</code>; monta <code>./agents:ro</code>, <code>./policies:ro</code>, <code>./data:rw</code>; <code>DATA_DIR=/app/data</code>, etc. El dashboard depende de <code>core: service_healthy</code> y usa <code>AGENTFORGE_CORE_URL=http://core:8000</code>. La imagen del core instala <code>en_core_web_sm</code> de spaCy para Presidio.</p>
|
||
</div>
|
||
<div>
|
||
<h4>Variables de entorno (<code>config.py</code> · <code>.env</code>)</h4>
|
||
<div class="tbl-wrap">
|
||
<table>
|
||
<thead><tr><th>Var</th><th>Default</th><th>Lo usa</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>LLM_PROVIDER</code></td><td><code>mock</code></td><td><code>build_llm_provider</code> (mock|azure|openai)</td></tr>
|
||
<tr><td><code>LLM_FALLBACK_PROVIDER</code></td><td><code>""</code></td><td>declarado; la factory aún no lo aplica</td></tr>
|
||
<tr><td><code>AZURE_OPENAI_*</code></td><td><code>""</code> / <code>2024-08-01-preview</code></td><td><code>AzureOpenAIProvider</code></td></tr>
|
||
<tr><td><code>OPENAI_API_KEY</code> · <code>OPENAI_MODEL</code></td><td><code>""</code> · <code>gpt-4o</code></td><td><code>OpenAIProvider</code></td></tr>
|
||
<tr><td><code>GUARDRAILS_NEMO_ENABLED</code></td><td><code>false</code></td><td><code>build_guardrail_engine</code> (añade NeMo al composite)</td></tr>
|
||
<tr><td><code>LOG_LEVEL</code></td><td><code>INFO</code></td><td><code>configure_logging</code></td></tr>
|
||
<tr><td><code>DATA_DIR</code></td><td><code>./data</code></td><td>checkpointer · índice · JSONL</td></tr>
|
||
<tr><td><code>AGENTS_DIR</code></td><td><code>./agents</code></td><td><code>FileSystemAgentRegistry</code></td></tr>
|
||
<tr><td><code>POLICIES_DIR</code></td><td><code>./policies</code></td><td><code>FileSystemPolicyStore</code></td></tr>
|
||
<tr><td><code>AGENTFORGE_CORE_URL</code></td><td><code>http://core:8000</code></td><td>el dashboard (<code>CoreClient</code>)</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="callout warn">
|
||
<div class="ct">⚠️ Gotchas</div>
|
||
<p><code>detect_pii</code> se comporta distinto según el entorno: con <code>presidio-analyzer</code> instalado (imagen Docker) usa Presidio + <code>en_core_web_sm</code>; sin él (venv local típico), regex fallback. · <code>AsyncSqliteSaver</code> liga su conexión al event loop activo: por eso el checkpointer es un <em>async context manager</em> que el orchestrator abre y cierra en cada operación; requiere <code>aiosqlite<0.21</code>. · <code>data/</code> debe existir y ser escribible por el usuario del contenedor (<code>agent</code>, uid 1000). · <code>build_llm_provider</code> usa <code>match</code> sin <code>case _</code> (exhaustivo sobre el <code>Literal</code> de 3 valores). · NeMo solo entra al composite si <code>GUARDRAILS_NEMO_ENABLED=true</code>.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="tests">
|
||
<h2><span class="kicker">18</span> Tests</h2>
|
||
<p>Hay una suite de <strong>unit + integration</strong> (~90 tests). <code>make test</code> corre solo los unit; <code>make test-all</code> añade los de integración; <code>make lint</code> es <code>ruff</code> + <code>mypy</code> (estricto); <code>make smoke</code> levanta <code>docker compose</code> y hace <code>curl /health</code>.</p>
|
||
<ul>
|
||
<li><strong><code>tests/unit/</code></strong> — dominio (<code>agent</code>, <code>execution</code>, <code>guardrail</code>, <code>policy</code>), <code>config</code>, <code>llm</code> (base, mock, factory), <code>guardrails</code> (ai, composite), <code>registry</code> (repository, policy_store, versioning, factory), <code>runtime</code> (state-graph, nodes, orchestrator), <code>observability/logging</code>, y los routers de la API (<code>agents</code>, <code>executions</code>, <code>policies/violations</code>, <code>health</code>) — estos usan <code>tests/fixtures/</code> (versiones mínimas) y hacen <code>deps.*.cache_clear()</code> tras apuntar <code>DATA_DIR</code> a un <code>tmp_path</code>.</li>
|
||
<li><strong><code>tests/integration/</code></strong> — montan la app con <code>TestClient</code> contra los <em>assets reales</em> del repo (<code>agents/</code>, <code>policies/</code>): <code>test_invoke_happy_path</code>, <code>test_invoke_hitl</code> (el ciclo completo en ~40 líneas — el mejor sitio para entenderlo), <code>test_invoke_pii_block</code>, <code>test_invoke_resume_after_restart</code> (prueba que un <code>awaiting_approval</code> sobrevive a recrear el orchestrator).</li>
|
||
<li>El dashboard Streamlit no se prueba con unit tests; se verifica con la checklist de <code>docs/manual_qa.md</code>.</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="endpoint">
|
||
<h2><span class="kicker">19</span> Este documento — un HTML autocontenido</h2>
|
||
<p>Lo que estás leyendo es un <strong>único fichero HTML autocontenido</strong> — <code>docs/walkthrough.html</code> en el repositorio. Todo está embebido: los estilos, los nueve diagramas SVG, la navegación con scrollspy, el tema claro/oscuro y los bloques de código. No depende de ninguna red ni de ningún CDN, y <strong>no necesita que ningún servidor esté corriendo</strong>: se abre directamente en cualquier navegador.</p>
|
||
<div class="code">
|
||
<div class="hd"><span class="dot"></span><span class="fn">abrir el walkthrough — no hace falta docker ni el core</span><span class="lang">bash</span></div>
|
||
<pre>xdg-open docs/walkthrough.html <span class="c"># Linux</span>
|
||
open docs/walkthrough.html <span class="c"># macOS</span>
|
||
<span class="c"># o arrástralo al navegador · o file://<ruta-del-repo>/docs/walkthrough.html</span></pre>
|
||
</div>
|
||
<div class="callout tip">
|
||
<div class="ct">🔌 ¿Servirlo desde la API?</div>
|
||
<p>Sería trivial si se quisiera: un router de FastAPI de ~10 líneas que devuelva este fichero como <code>HTMLResponse</code> (p. ej. bajo <code>GET /walkthrough</code>), montado aparte de los routers de negocio. En esta versión del repo no está cableado — el documento se consulta como fichero.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="glosario">
|
||
<h2><span class="kicker">20</span> Glosario</h2>
|
||
<div class="tbl-wrap">
|
||
<table>
|
||
<thead><tr><th>Término</th><th>Significado en este proyecto</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><strong>Agente</strong></td><td>Una configuración declarativa (YAML): prompt de sistema, modelo LLM, esquema de salida, lista de políticas de guardrails, umbral de riesgo para HITL. No es código.</td></tr>
|
||
<tr><td><strong>Política (de guardrails)</strong></td><td>Un YAML con la lista de validadores de entrada y salida y su config, más <code>on_validator_error</code> (<code>fail_closed</code>/<code>fail_open</code>).</td></tr>
|
||
<tr><td><strong>Validador / guardrail</strong></td><td>Una función que mira el texto de entrada o la salida del LLM y devuelve cero o más <code>GuardrailViolation</code>. Ej.: <code>detect_pii</code>, <code>prompt_injection</code>, <code>schema_match</code>, <code>telco_safety_rules</code>.</td></tr>
|
||
<tr><td><strong>Violación</strong></td><td>El resultado de un validador: <code>stage</code> (input/output), <code>validator</code>, <code>severity</code> (<code>info</code>/<code>warning</code>/<code>block</code>), <code>message</code>, <code>blocked</code>.</td></tr>
|
||
<tr><td><strong>Ejecución (<code>AgentExecution</code>)</strong></td><td>El "expediente" de una invocación: <code>trace_id</code>, status, <code>decision_path</code>, <code>violations</code>, <code>proposed_actions</code>, <code>needs_human_for</code>, <code>final_output</code>, <code>error</code>.</td></tr>
|
||
<tr><td><strong><code>decision_path</code></strong></td><td>La lista de pasos por los que pasó el grafo, cada uno con <code>step</code>, <code>timestamp</code>, <code>duration_ms</code> y un <code>detail</code>. La "caja negra" de la ejecución.</td></tr>
|
||
<tr><td><strong>HITL</strong></td><td>Human-in-the-Loop: pausar la ejecución cuando hay una acción arriesgada y esperar a que un humano apruebe o rechace. Implementado con <code>interrupt()</code> de LangGraph.</td></tr>
|
||
<tr><td><strong>Checkpointer</strong></td><td>El componente de LangGraph que persiste el estado del grafo (aquí, en SQLite). Lo que hace posible reanudar un HITL tras un reinicio.</td></tr>
|
||
<tr><td><strong><code>trace_id</code></strong></td><td>UUID que identifica una petición/ejecución y se propaga por middleware → logs → API → ficheros de auditoría.</td></tr>
|
||
<tr><td><strong>Orchestrator</strong></td><td>La única clase que habla con LangGraph (<code>invoke</code>/<code>resume</code>/<code>snapshot</code>); aísla al resto del sistema del runtime.</td></tr>
|
||
<tr><td><strong>Factory</strong></td><td>Función <code>build_X(settings)</code> que monta la implementación de <code>X</code> adecuada según la configuración.</td></tr>
|
||
<tr><td><strong>Status de ejecución</strong></td><td><code>running</code>, <code>awaiting_approval</code> (pausada en HITL), <code>blocked_by_guardrail</code>, <code>completed</code>, <code>failed</code>.</td></tr>
|
||
<tr><td><strong>Estados de un agente</strong></td><td><code>draft</code>, <code>active</code>, <code>deprecated</code> (metadato en su YAML).</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="roadmap">
|
||
<h2><span class="kicker">21</span> Lo que aún no hace (a propósito)</h2>
|
||
<p>Es un MVP. En el roadmap (<code>docs/futuro.md</code>): <strong>NeMo Guardrails completo</strong> (Colang + KB embedding + dialog rails) · <strong>autenticación</strong> (OAuth2/OIDC, MSAL para Azure AD) · <strong>OpenTelemetry</strong> (spans por nodo, métricas de violaciones por validador) · <strong>multi-tenant</strong> (<code>tenant_id</code> aislado) · <strong>evaluadores LLM-as-judge</strong> (regression suite sobre escenarios canónicos, mide deriva) · <strong>UI de aprobación con SLA</strong> (cola Kanban, reasignación, alertas) · <strong>persistencia migrable a Postgres + pgvector</strong> · <strong>esquema de prompts mejorado</strong> (variables tipadas tipo Jinja, valores por entorno) · <strong>promoción explícita draft → active</strong> vía PR/aprobación. La detección de PII más fina (modelos grandes, español), también pendiente.</p>
|
||
</section>
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="leer">
|
||
<h2><span class="kicker">22</span> Por dónde empezar a leer (ruta de ~1 hora)</h2>
|
||
<ol>
|
||
<li><code>domain/execution.py</code> y <code>domain/agent.py</code> — qué datos hay. <em>(§<a href="#dominio">06</a>)</em></li>
|
||
<li><code>policies/default/versions/v1.yaml</code> y <code>agents/incident_analyzer/versions/v2.yaml</code> — cómo se declara todo. <em>(§<a href="#guardrails">07</a>, §<a href="#versionado">10</a>)</em></li>
|
||
<li><code>runtime/nodes.py</code> y <code>runtime/graph.py</code> — el flujo de ejecución. <em>(§<a href="#runtime">08</a>)</em></li>
|
||
<li><code>api/executions.py</code> — cómo se expone (invoke / approve / reject). <em>(§<a href="#api">14</a>)</em></li>
|
||
<li><code>tests/integration/test_invoke_hitl.py</code> — el ciclo completo, en ~40 líneas. <em>(§<a href="#tests">18</a>)</em></li>
|
||
<li>Levanta <code>docker compose up</code>, haz clic por las cinco páginas del dashboard <em>(§<a href="#dashboard">16</a>)</em>, y abre <code>docs/walkthrough.html</code> en el navegador <em>(§<a href="#endpoint">19</a>)</em>.</li>
|
||
</ol>
|
||
<div class="callout key">
|
||
<div class="ct">📚 Documentos hermanos</div>
|
||
<p><code>README.md</code> — quickstart y estado. · <code>ARCHITECTURE.md</code> — decisiones técnicas en bruto. · <code>docs/explicacion.md</code> — la narrativa de extremo a extremo (en prosa). · <code>docs/componentes.md</code> — la referencia de cableado a bajo nivel (firmas exactas, grafos de dependencias, cadenas de llamada por endpoint). · <code>docs/futuro.md</code> — roadmap. · <code>docs/manual_qa.md</code> — checklist de verificación manual del dashboard.</p>
|
||
</div>
|
||
</section>
|
||
|
||
|
||
<!-- ============================================================= -->
|
||
<section id="codificar">
|
||
<h2><span class="kicker">23</span> Ruta de codificación manual — de cero a sistema funcionando</h2>
|
||
<p class="lead">Si quieres <strong>entender el proyecto escribiéndolo</strong>, este es el orden que maximiza la comprensión. La lógica: sigue el grafo de dependencias hacia afuera desde el centro — empieza por lo que no depende de nada, termina por lo que depende de todo.</p>
|
||
|
||
<div class="callout key">
|
||
<div class="ct">Principio rector</div>
|
||
<p><code>domain/</code> no importa nada del proyecto. El grafo no sabe que existe una API. La API no sabe que existe el dashboard. Escribe en esa dirección: <strong>adentro → afuera, abstracto → concreto, sin dependencias → con dependencias</strong>.</p>
|
||
</div>
|
||
|
||
<h3>Fase 1 — El vocabulario: <code>domain/</code> <em class="muted">(~30 min)</em></h3>
|
||
<p>Estos cuatro ficheros no importan nada del proyecto — solo Pydantic. Son el vocabulario de todo lo demás. Escribirlos primero te obliga a responder <em>"¿qué maneja este sistema?"</em> antes de pensar en cómo lo procesa.</p>
|
||
<ol>
|
||
<li><code>domain/agent.py</code> — <code>AgentDefinition</code>, <code>LLMConfig</code>, <code>AgentVersionMeta</code>. Al terminar sabrás exactamente qué es "un agente" en este sistema: una declaración Pydantic, no código Python.</li>
|
||
<li><code>domain/guardrail.py</code> — <code>GuardrailViolation</code>. Qué devuelve un guardrail cuando detecta un problema: una violación tipada, no una excepción.</li>
|
||
<li><code>domain/policy.py</code> — <code>PolicyDefinition</code>. La lista de reglas que rigen un agente concreto.</li>
|
||
<li><code>domain/execution.py</code> — el modelo más rico: <code>AgentExecution</code>, <code>ProposedAction</code>, <code>ExecutionStatus</code>, <code>DecisionStep</code>. Contiene el estado completo de una ejecución. Fíjate en que <code>AgentExecution</code> tiene <em>definición</em> del agente separada de su <em>instancia en runtime</em>.</li>
|
||
</ol>
|
||
<div class="callout tip">
|
||
<div class="ct">Momento clave #1</div>
|
||
<p>Al escribir <code>AgentExecution</code> te das cuenta de que el sistema separa <em>definir</em> un agente (YAML → Pydantic) de <em>ejecutarlo</em> (runtime → JSONL). Son dos ciclos de vida distintos.</p>
|
||
</div>
|
||
|
||
<h3>Fase 2 — Las costuras: los dos <code>Protocol</code>s <em class="muted">(~15 min)</em></h3>
|
||
<p>Los <code>Protocol</code>s son las interfaces estructurales del sistema — el contrato que separa <em>"qué hace"</em> de <em>"cómo lo hace"</em>. El grafo solo importa estos dos ficheros, nunca OpenAI ni guardrails-ai directamente.</p>
|
||
<ol>
|
||
<li><code>llm/base.py</code> — <code>LLMProvider</code> (un método <code>async complete()</code>), <code>Message</code>, <code>CompletionResult</code>. Dos clases Pydantic y un Protocol de cuatro líneas.</li>
|
||
<li><code>guardrails/base.py</code> — <code>GuardrailEngine</code>: dos métodos (<code>validate_input</code> y <code>validate_output</code>), ambos devuelven <code>list[GuardrailViolation]</code>.</li>
|
||
</ol>
|
||
<div class="callout tip">
|
||
<div class="ct">Momento clave #2</div>
|
||
<p>Escribir estos dos Protocols antes que cualquier implementación te revela por qué cambiar el proveedor LLM de <code>mock</code> a <code>azure</code> no requiere tocar el grafo: el grafo nunca ve la implementación concreta.</p>
|
||
</div>
|
||
|
||
<h3>Fase 3 — Primera ejecución posible: mocks + YAML del agente <em class="muted">(~45 min)</em></h3>
|
||
<p>Con dominios e interfaces ya puedes ejecutar el sistema sin ninguna clave de API. Los mocks son tu andamio.</p>
|
||
<ol>
|
||
<li><code>llm/mock.py</code> — implementa <code>LLMProvider</code> devolviendo un JSON hardcodeado. ~30 líneas. Permite correr el grafo completo sin tocar OpenAI ni Azure.</li>
|
||
<li><code>guardrails/validators.py</code> — validadores keyword-based simples. Implementan <code>GuardrailEngine</code> sin dependencias externas.</li>
|
||
<li>El primer YAML del agente: <code>agents/incident_analyzer/versions/v1.yaml</code>. Escribe la definición declarativa (nombre, owner, purpose, guardrails, config LLM, system_prompt, output_schema).</li>
|
||
</ol>
|
||
<div class="callout tip">
|
||
<div class="ct">Momento clave #3</div>
|
||
<p>Al escribir el YAML del agente entiendes que un agente es <strong>pura configuración</strong>: no tiene código Python. Cambiar su comportamiento — modelo, temperatura, guardrails, threshold de HITL — es editar texto, no redeployar.</p>
|
||
</div>
|
||
|
||
<h3>Fase 4 — El corazón del sistema: estado → nodos → grafo <em class="muted">(~1.5 h)</em></h3>
|
||
<p>Esta es la parte más densa e importante. El grafo LangGraph es la máquina de estados que orquesta todo. El orden dentro de la fase también importa.</p>
|
||
<ol>
|
||
<li>
|
||
<strong><code>runtime/state.py</code></strong> primero — <code>AgentState</code>: un <code>TypedDict</code> con todos los campos del estado mutable que fluye por el grafo.
|
||
El detalle a entender: <code>decision_path: Annotated[list, operator.add]</code> — no se sobreescribe, se acumula; LangGraph llama al reducer automáticamente cada vez que un nodo devuelve un <code>decision_path</code>.
|
||
</li>
|
||
<li>
|
||
<strong><code>runtime/nodes.py</code></strong> — los seis nodos, en el orden en que aparecerán en el grafo:
|
||
<ul>
|
||
<li><code>build_node_validate_input</code> — llama al engine, acumula violations, decide si poner <code>blocked_by_guardrail</code>.</li>
|
||
<li><code>build_node_llm_reason</code> — llama al provider, captura el fallo y lo transforma en <code>status="failed"</code>.</li>
|
||
<li><code>build_node_validate_output</code> — parsea el JSON del LLM y valida el output contra la política.</li>
|
||
<li><code>build_node_propose_actions</code> — extrae <code>proposed_actions</code> del output parseado.</li>
|
||
<li><code>build_node_approve_gate</code> — el nodo HITL: llama a <code>interrupt({"awaiting_actions": risky})</code> si hay acciones con riesgo ≥ threshold. La ejecución se pausa aquí hasta que llegue un <code>Command(resume=...)</code>.</li>
|
||
<li><code>build_node_finalize</code> — filtra acciones por las aprobadas, construye <code>final_output</code>.</li>
|
||
</ul>
|
||
<p>Nota el patrón: cada builder <em>captura sus dependencias por clausura</em> y devuelve una función <code>async (state) → dict</code>. Las dependencias no viajan en el estado — están fijas en el cierre.</p>
|
||
</li>
|
||
<li>
|
||
<strong><code>runtime/graph.py</code></strong> — conecta los nodos. Los tres routers condicionales (<code>_after_validate_input</code>, <code>_after_llm</code>, <code>_after_validate_output</code>) hacen que cualquier error cortocircuite hacia <code>END</code> sin pasar por nodos posteriores.
|
||
</li>
|
||
</ol>
|
||
<div class="callout tip">
|
||
<div class="ct">Momento clave #4</div>
|
||
<p>Al implementar <code>approve_gate</code> entiendes cómo funciona HITL en LangGraph: <code>interrupt()</code> no lanza excepción — serializa el estado en el checkpoint SQLite y devuelve el control. La ejecución se reanuda en el mismo nodo cuando llega <code>Command(resume=...)</code>. El estado nunca se pierde entre las dos llamadas HTTP.</p>
|
||
</div>
|
||
|
||
<h3>Fase 5 — Orquestador y checkpointer <em class="muted">(~30 min)</em></h3>
|
||
<ol>
|
||
<li><code>runtime/checkpointer.py</code> — configura SQLite como backend de checkpoints LangGraph. Sin esto, <code>interrupt()</code> no puede persistir el estado entre la llamada a <code>invoke</code> y la llamada a <code>approve</code>.</li>
|
||
<li><code>runtime/orchestrator.py</code> — el objeto que la API usará. Recibe <code>AgentDefinition</code> + <code>PolicyDefinition</code> + providers ya construidos, compila el grafo y expone dos métodos: <code>invoke()</code> (nueva ejecución) y <code>approve()</code> (reanudar tras HITL).</li>
|
||
</ol>
|
||
<p>Punto de control: en este momento tienes un sistema funcionando de extremo a extremo. Puedes llamar directamente al orquestador en un test y observar el <code>decision_path</code> completo, sin API ni dashboard.</p>
|
||
|
||
<h3>Fase 6 — La API: de adentro hacia afuera <em class="muted">(~1 h)</em></h3>
|
||
<p>Dentro de <code>api/</code>, el orden también importa.</p>
|
||
<ol>
|
||
<li><code>api/deps.py</code> — el armario de cableado: los <code>@lru_cache</code> que construyen el registry, el policy store, el LLM provider y el guardrail engine. <strong>Escríbelo después de tener todo lo anterior</strong> — solo entonces sabrás exactamente qué estás conectando.</li>
|
||
<li><code>api/executions.py</code> — los tres endpoints críticos: <code>POST /executions/invoke</code>, <code>POST /executions/{trace_id}/approve</code>, <code>GET /executions/{trace_id}</code>.</li>
|
||
<li><code>api/agents.py</code> — CRUD del registry (listar agentes, leer versión concreta).</li>
|
||
<li><code>api/middlewares.py</code> — inyecta <code>X-Trace-Id</code> y bind-ea el contexto de structlog.</li>
|
||
<li><code>main.py</code> — monta los routers, configura CORS, arranca la app FastAPI.</li>
|
||
</ol>
|
||
|
||
<h3>Fase 7 — Registry y versionado <em class="muted">(~45 min)</em></h3>
|
||
<p>Si has estado usando el mock registry, ahora añades la persistencia real basada en YAML.</p>
|
||
<ol>
|
||
<li><code>registry/repository.py</code> — carga definiciones de agentes desde YAML, cachea en memoria.</li>
|
||
<li><code>registry/versioning.py</code> — hash SHA-256 + <code>index.yaml</code>: el sistema de versionado tipo-Git.</li>
|
||
<li><code>registry/policy_store.py</code> — análogo al repository, pero para políticas.</li>
|
||
<li><code>registry/factory.py</code> — el factory que elige qué implementación de registry usar según <code>Settings</code>.</li>
|
||
</ol>
|
||
|
||
<h3>Fase 8 — El dashboard <em class="muted">(~1.5 h)</em></h3>
|
||
<p>El dashboard es un cliente HTTP del core — puedes empezarlo en cualquier momento después de tener los endpoints.</p>
|
||
<ol>
|
||
<li><code>dashboard/client.py</code> — primero. Te fuerza a pensar en qué necesita la UI antes de diseñar las páginas. Es el contrato del core visto desde el consumidor.</li>
|
||
<li>Las páginas por complejidad creciente:
|
||
<ul>
|
||
<li><code>pages/1_Registro.py</code> — solo lectura: lista agentes y versiones.</li>
|
||
<li><code>pages/5_Politicas.py</code> — solo lectura: muestra políticas activas.</li>
|
||
<li><code>pages/4_Historial.py</code> — lectura + filtros: lista ejecuciones pasadas.</li>
|
||
<li><code>pages/2_Ejecutar.py</code> — escritura: invoke + polling de estado + visualización del <code>decision_path</code>.</li>
|
||
<li><code>pages/3_Aprobaciones.py</code> — la más compleja: muestra acciones pendientes, permite aprobar o rechazar individual o masivamente.</li>
|
||
</ul>
|
||
</li>
|
||
</ol>
|
||
|
||
<div class="callout ok">
|
||
<div class="ct">Punto de control mínimo viable (tras Fase 5)</div>
|
||
<p>Con las fases 1–5 completas y el mock LLM tienes un sistema funcionando sin Docker ni credenciales externas. Llama directamente al orquestador desde un test de integración, observa el <code>decision_path</code> completo, y pausa-reanuda en el nodo HITL. No necesitas la API ni el dashboard para validar el núcleo.</p>
|
||
</div>
|
||
</section>
|
||
|
||
|
||
<div class="footer">
|
||
<p><strong>AgentForge · Walkthrough.</strong> Documento autocontenido (sin recursos externos). Acompaña a <code>README.md</code>, <code>ARCHITECTURE.md</code>, <code>docs/explicacion.md</code> (narrativa) y <code>docs/componentes.md</code> (referencia de cableado). Refleja el repo en <code>v0.1.0</code>.</p>
|
||
<p class="muted">Documento HTML autocontenido (<code>docs/walkthrough.html</code>) — sin recursos externos: ábrelo en cualquier navegador.</p>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<button id="totop" aria-label="Volver arriba">↑</button>
|
||
|
||
<script>
|
||
(function(){
|
||
// ---- theme toggle ----
|
||
function getTheme(){ return document.documentElement.getAttribute('data-theme') || 'dark'; }
|
||
function setTheme(t){ document.documentElement.setAttribute('data-theme', t); try{localStorage.setItem('af-theme', t);}catch(e){} }
|
||
function toggle(){ setTheme(getTheme()==='dark'?'light':'dark'); }
|
||
['themeBtn','themeBtnM'].forEach(function(id){ var b=document.getElementById(id); if(b) b.addEventListener('click', toggle); });
|
||
|
||
// ---- mobile sidebar ----
|
||
var sb=document.getElementById('sidebar'), scrim=document.getElementById('scrim'), mb=document.getElementById('menuBtn');
|
||
function openSb(){ sb.classList.add('open'); scrim.classList.add('show'); }
|
||
function closeSb(){ sb.classList.remove('open'); scrim.classList.remove('show'); }
|
||
if(mb) mb.addEventListener('click', openSb);
|
||
if(scrim) scrim.addEventListener('click', closeSb);
|
||
sb.addEventListener('click', function(e){ if(e.target.tagName==='A') closeSb(); });
|
||
|
||
// ---- back to top ----
|
||
var tt=document.getElementById('totop');
|
||
tt.addEventListener('click', function(){ window.scrollTo({top:0, behavior:'smooth'}); });
|
||
window.addEventListener('scroll', function(){ tt.classList.toggle('show', window.scrollY>600); }, {passive:true});
|
||
|
||
// ---- scrollspy ----
|
||
var links=Array.prototype.slice.call(document.querySelectorAll('#toc a'));
|
||
var map={}; links.forEach(function(a){ var id=a.getAttribute('href').slice(1); map[id]=a; });
|
||
var secs=Object.keys(map).map(function(id){ return document.getElementById(id); }).filter(Boolean);
|
||
var current=null;
|
||
var io=new IntersectionObserver(function(entries){
|
||
entries.forEach(function(en){
|
||
if(en.isIntersecting){
|
||
if(current) current.classList.remove('active');
|
||
current=map[en.target.id]; if(current){ current.classList.add('active');
|
||
// keep active link in view within the sidebar
|
||
var r=current.getBoundingClientRect(), pr=sb.getBoundingClientRect();
|
||
if(r.top<pr.top+50 || r.bottom>pr.bottom-20) current.scrollIntoView({block:'nearest'});
|
||
}
|
||
}
|
||
});
|
||
}, {rootMargin:'-12% 0px -78% 0px', threshold:0});
|
||
secs.forEach(function(s){ io.observe(s); });
|
||
|
||
// ---- copy buttons ----
|
||
document.querySelectorAll('.code').forEach(function(box){
|
||
var btn=document.createElement('button'); btn.className='cp'; btn.textContent='copiar';
|
||
var hd=box.querySelector('.hd'); if(!hd){ return; }
|
||
hd.appendChild(btn);
|
||
btn.addEventListener('click', function(){
|
||
var pre=box.querySelector('pre'); var txt=pre?pre.innerText:'';
|
||
navigator.clipboard.writeText(txt).then(function(){ btn.textContent='✓ copiado'; setTimeout(function(){btn.textContent='copiar';},1400); });
|
||
});
|
||
});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|