fivetech commited on
Commit
ff4bfc3
·
verified ·
1 Parent(s): 66b4dc1

Create commands.mjs

Browse files
Files changed (1) hide show
  1. core/commands.mjs +90 -0
core/commands.mjs ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const APCorePrompts = {
2
+ ALPHA: 'Eres ALPHA, agente de inventario de FiveTech. Responde en español con hechos del contexto. Puedes proponer reposición con la herramienta si el stock está bajo. Nunca digas que has comprado ni recibido mercancía. Describe exactamente el estado de la propuesta en SQLite y el modo actual. No inventes datos ni atribuyas acciones a BETA/GAMMA si no constan en la auditoría. En modo Autónomo el cumplimiento actual exige revisión humana de toda propuesta local nueva; nunca se compra ni se recibe mercancía. No reveles proveedores de modelos.',
3
+ BETA: 'Eres BETA, agente de pedidos de FiveTech. Responde en español solo con hechos de los datos vigentes del contexto: analiza la cartera de pedidos y el riesgo de inventario sin inferir cobros, pagos ni envíos. Nunca digas que has comprado, vendido ni recibido mercancía. No atribuyas acciones a ALPHA o GAMMA si no constan en la auditoría. No crees propuestas ni uses herramientas: las propuestas de reposición son de ALPHA. No reveles proveedores de modelos.',
4
+ GAMMA: 'Eres GAMMA, agente de proveedores de FiveTech. Responde en español solo con hechos de los datos vigentes del contexto: compara proveedores, plazos y costes y señala el proveedor que corresponda. Nunca prometas entregas ni envíos que no consten, ni digas que has comprado ni recibido mercancía. No atribuyas acciones a ALPHA o BETA si no constan en la auditoría. No crees propuestas ni usas herramientas: la propuesta la crea ALPHA con el proveedor seleccionado. No reveles proveedores de modelos.',
5
+ };
6
+ export const APCommands = {
7
+ async reset(){if(!await AP.confirmAction('Restablecer solo esta demo','Esto borra permanentemente pedidos, propuestas, decisiones, perfiles y movimientos locales de este navegador y vuelve a cargar el escenario ficticio de julio de 2030. No afecta otros equipos ni sistemas reales.'))return;await new Promise((resolve,reject)=>{const t=AP.store.transaction('database','readwrite');t.objectStore('database').delete('main');t.oncomplete=resolve;t.onerror=()=>reject(t.error);});AP.db?.close();AP.db=null;localStorage.removeItem('adaptapro-notices-read');await AP.init();AP.audit('SISTEMA','demo_restablecida','settings','reset',{origen:'boton_menu'});await AP.persist();AP.note('Datos locales restablecidos.');},
8
+ async decide(id,status){
9
+ const p=AP.one('SELECT * FROM purchase_proposals WHERE id=?',[id]);if(!p)return;
10
+ const check=APCompliance.evaluate(AP,{sku:p.sku,qty:p.proposed_qty,supplier_id:p.supplier_id});if(status==='aprobada'&&check.status==='block'){AP.note('La propuesta sigue bloqueada. '+check.reason,true);return;}
11
+ const input=await AP.decisionForm(`${status==='aprobada'?'Revisión humana y aprobación local':'Rechazar'} ${id}`,`${p.proposed_qty} unidades de ${p.sku} · ${AP.money(p.proposed_qty*p.unit_cost_cents)}. Estado de cumplimiento: ${check.status==='review'?'REVISAR (no conforme certificado)':check.status}. ${check.norm_id||'Sin norma'}: ${check.reason}. Si apruebas, solo cambia el estado de esta propuesta local: no certifica legalidad, no compra ni registra recepción. Indica responsable y motivo de tu revisión.`,true);if(!input)return;if(!input.actor||!input.reason){AP.note('Es obligatorio indicar responsable y motivo de la revisión humana.',true);return;}
12
+ try{const refreshed=APCompliance.evaluate(AP,{sku:p.sku,qty:p.proposed_qty,supplier_id:p.supplier_id});if(status==='aprobada'&&refreshed.status==='block')throw Error('Bloqueo de cumplimiento vigente');AP.tx(()=>{const changes=AP.exec({sql:'UPDATE approval_queue SET status=?,decided_by=?,decided_at=?,decision_reason=? WHERE proposal_id=? AND status=?',args:[status,input.actor,new Date().toISOString(),input.reason,id,'pendiente'],actor:input.actor,action:'decision_'+status,entity:'purchase_proposal',id,detail:{reason:input.reason,compliance_status:refreshed.status,compliance_norm_id:refreshed.norm_id,compliance_reason:refreshed.reason,human_review:status==='aprobada'&&refreshed.status==='review'}});if(!changes)throw Error('La propuesta ya se había decidido');});await AP.persist();AP.render();AP.note('Decisión local registrada: '+status+(status==='aprobada'&&refreshed.status==='review'?' tras revisión humana; no certifica cumplimiento jurídico.':'.'));if(status==='aprobada')APSound.play('approval');}catch(e){AP.note(e.message,true);}
13
+ },
14
+ async receive(id){
15
+ const p=AP.one(`SELECT p.* FROM purchase_proposals p JOIN approval_queue q ON q.proposal_id=p.id WHERE p.id=? AND q.status='aprobada'`,[id]);if(!p)return;
16
+ const input=await AP.decisionForm('Registrar recepción',`${p.proposed_qty} unidades de ${p.sku}. Esto aumentará el stock local; registra la recepción solo cuando la entrega se haya comprobado.`,false);if(!input)return;
17
+ try{AP.tx(()=>{if(AP.one('SELECT id FROM stock_movements WHERE proposal_id=?',[id]))throw Error('Recepción ya registrada');const now=new Date().toISOString();AP.exec({sql:'UPDATE inventory SET on_hand=on_hand+?,updated_at=? WHERE sku=?',args:[p.proposed_qty,now,p.sku],audit:false});AP.exec({sql:'INSERT INTO stock_movements VALUES(?,?,?,?,?,?)',args:['MOV-'+id,p.sku,p.proposed_qty,'recepcion_registrada',id,now],actor:input.actor,action:'recepcion_registrada',entity:'purchase_proposal',id,detail:{sku:p.sku,quantity:p.proposed_qty}});});await AP.persist();AP.render();AP.note('Recepción registrada; existencias actualizadas.');}catch(e){AP.note(e.message,true);}
18
+ },
19
+ async propose(args){
20
+ const sku=String(args.sku||'');const x=AP.one('SELECT * FROM stock_status WHERE sku=?',[sku]);if(!x||!x.needs_restock)throw Error('SKU inexistente o sin necesidad de reposición');
21
+ if(AP.one("SELECT p.id FROM purchase_proposals p JOIN approval_queue q ON q.proposal_id=p.id WHERE p.sku=? AND q.status='pendiente'",[sku]))return 'Ya hay una propuesta pendiente para '+sku;
22
+ AP.setAgentState('gamma','Comparando proveedores',true);
23
+ const supplier=AP.one('SELECT sp.*,s.name,s.lead_days FROM supplier_products sp JOIN suppliers s ON s.id=sp.supplier_id WHERE sp.sku=? AND s.active=1 ORDER BY s.lead_days ASC,sp.unit_cost_cents ASC LIMIT 1',[sku]);AP.setAgentState('gamma',supplier?`Plazo ${supplier.lead_days} días`:'Sin proveedor',false);if(!supplier)throw Error('No hay proveedor activo');
24
+ const qty=Math.max(x.target_stock-x.available,supplier.min_order_qty);const id='PROP-'+Date.now();
25
+ const compliance=APCompliance.evaluate(AP,{sku,qty,supplier_id:supplier.supplier_id});if(compliance.status==='block')throw Error('Propuesta detenida por cumplimiento: '+compliance.reason);const auto=AP.mode==='autonomo'&&compliance.status==='pass';AP.tx(()=>{const now=new Date().toISOString();AP.exec({sql:'INSERT INTO purchase_proposals VALUES(?,?,?,?,?,?,?,?,?)',args:[id,sku,supplier.supplier_id,qty,supplier.unit_cost_cents,'USD','ALPHA',String(args.reason||'Stock por debajo del punto de reposición').slice(0,500),now],actor:'ALPHA',action:'propuesta_creada',entity:'purchase_proposal',id,detail:{sku,quantity:qty}});AP.exec({sql:'INSERT INTO approval_queue(proposal_id,status,decided_by,decided_at,decision_reason) VALUES(?,?,?,?,?)',args:[id,auto?'aprobada':'pendiente',auto?'modo_automatico':null,auto?now:null,auto?'Aprobación automática local: cumplimiento pass':null],audit:false});AP.audit('GAMMA','proveedor_seleccionado','supplier',supplier.supplier_id,{sku,lead_days:supplier.lead_days,unit_cost_cents:supplier.unit_cost_cents});AP.exec({sql:'INSERT INTO compliance_checks(proposal_id,status,norm_id,reason,checked_at) VALUES(?,?,?,?,?)',args:[id,compliance.status,compliance.norm_id,compliance.reason,now],audit:false});AP.audit('CUMPLIMIENTO','revision_normativa','purchase_proposal',id,{status:compliance.status,norm_id:compliance.norm_id,reason:compliance.reason});if(auto)AP.audit('modo_automatico','aprobacion_automatica','purchase_proposal',id,{sku,quantity:qty,compliance_status:compliance.status,compliance_reason:compliance.reason});});await AP.persist();AP.render();if(auto)APSound.play('autonomous');return `Propuesta ${id} creada: ${qty} unidades de ${sku} por ${AP.money(qty*supplier.unit_cost_cents)}. ${auto?'Aprobada automáticamente en la base local tras cumplimiento pass.':'Pendiente de aprobación humana y revisión de cumplimiento;'} No se ha efectuado compra ni recepción.`;
26
+ },
27
+ // Único punto de conexión del modelo: cambiar endpoints, modelo o autenticación aquí.
28
+ async modelCall(messages,tools){
29
+ const request=async(endpoint,model)=>{
30
+ const body={model,messages,temperature:0.2,stream:false};if(tools)body.tools=tools;
31
+ const controller=new AbortController();const timeout=setTimeout(()=>controller.abort(),25000);
32
+ try{
33
+ const r=await fetch(endpoint+'/chat/completions',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer public'},body:JSON.stringify(body),signal:controller.signal});
34
+ if(!r.ok)throw Error('Servicio no disponible (HTTP '+r.status+')');
35
+ const j=await r.json();if(!j.choices?.[0]?.message)throw Error('Respuesta vacía');return j.choices[0].message;
36
+ }finally{clearTimeout(timeout);}
37
+ };
38
+ let last;
39
+ for(const endpoint of AP.endpoints){
40
+ let available=[];
41
+ try{const r=await fetch(endpoint+'/models',{headers:{'Authorization':'Bearer public'}});if(r.ok){const j=await r.json();available=(j.data||[]).map(x=>x.id).filter(x=>x.endsWith('-free'));}}catch(e){last=e;}
42
+ const candidates=AP.activeModel?[AP.activeModel,...available.filter(x=>x!==AP.activeModel)]:[AP.model,...available.filter(x=>x!==AP.model)];
43
+ for(const model of candidates){try{const response=await request(endpoint,model);AP.activeModel=model;return response;}catch(e){last=e;}}
44
+ }
45
+ throw last||Error('Servicio no disponible');
46
+ },
47
+ context(agent){const who=String(agent||'').toUpperCase();const base={stock:AP.query('SELECT * FROM stock_status'),orders:AP.query("SELECT l.order_id,l.sku,l.quantity,o.status,o.region FROM sales_order_lines l JOIN sales_orders o ON o.id=l.order_id"),proposals:AP.query('SELECT p.id,p.sku,p.proposed_qty,p.rationale,q.status FROM purchase_proposals p JOIN approval_queue q ON p.id=q.proposal_id')};if(who!=='ALPHA'&&who!=='BETA'&&who!=='GAMMA')return base;const providers=AP.query('SELECT s.id,s.name,s.lead_days,sp.sku,sp.unit_cost_cents,sp.min_order_qty FROM suppliers s JOIN supplier_products sp ON sp.supplier_id=s.id WHERE s.active=1');if(who==='ALPHA')return {...base,providers};if(who==='GAMMA')return {stock:base.stock,proposals:base.proposals,providers};return base;},
48
+ async ask(text,quiet=false,agent){
49
+ const who=['ALPHA','BETA','GAMMA'].includes(String(agent||'').toUpperCase())?String(agent).toUpperCase():'ALPHA';
50
+ const st=who.toLowerCase();
51
+ if(AP.busy||!AP.db)return;AP.busy=true;if(!quiet){AP.tab('chat');AP.append(text,'user');}AP.setAgentState(st,'Analizando',true);AP.note('Analizando con '+who+'...');
52
+ try{
53
+ const tools=[{type:'function',function:{name:'proponer_reposicion',description:'Crear una propuesta local de reposición. La revisión de cumplimiento actual la deja pendiente incluso en Autónomo; no compra ni cambia stock.',parameters:{type:'object',properties:{sku:{type:'string'},reason:{type:'string'}},required:['sku','reason']}}}];
54
+ const ctx=AP.context(who);const req=String(text);
55
+ if(/producci|fabricaci|materiales|taller/i.test(req))ctx.production=AP.query('SELECT * FROM production_jobs ORDER BY due_at');
56
+ if(/planific|tareas|prioridad|calendario|programaci/i.test(req))ctx.planning=AP.query('SELECT * FROM planning_tasks ORDER BY due_at');
57
+ const messages=[{role:'system',content:APCorePrompts[who]},{role:'user',content:'Modo actual: '+AP.mode+'. Datos vigentes: '+JSON.stringify(ctx)+'\nSolicitud: '+text}];
58
+ let m=await AP.modelCall(messages,who==='ALPHA'?tools:undefined);
59
+ if(who==='ALPHA'&&m.tool_calls?.length){messages.push(m);for(const c of m.tool_calls.slice(0,2)){let result;try{if(c.function.name!=='proponer_reposicion')throw Error('Herramienta no admitida');result=await AP.command({cmd:'propuesta_crear',payload:JSON.parse(c.function.arguments),actor:who});}catch(e){result='Error: '+e.message;}messages.push({role:'tool',tool_call_id:c.id,content:result});}m=await AP.modelCall(messages);}
60
+ if(!quiet)AP.append(m.content||'No hubo respuesta del agente.','ai');AP.audit(who,'analisis_completado','agent',who,{model:AP.activeModel||AP.model});await AP.persist();AP.render();AP.setAgentState(st,'Disponible',false);AP.note('Análisis finalizado · '+AP.query("SELECT count(*) AS n FROM approval_queue WHERE status='pendiente'")[0].n+' pendientes');
61
+ }catch(e){if(!quiet)AP.append('No se pudo contactar al agente. Tus datos y propuestas locales siguen disponibles.','ai');AP.setAgentState(st,'Sin respuesta',false);AP.note('Agente no disponible: '+e.message,true);}finally{AP.busy=false;}
62
+ },
63
+ async cycle(){
64
+ if(!AP.db||AP.cycleBusy||AP.busy||document.hidden||!AP.agentsOn)return;
65
+ const count=AP.one("SELECT count(*) AS n FROM audit_log WHERE action='pedido_generado'").n;
66
+ AP.cycleBusy=true;AP.setAgentState('beta','Registrando pedido',true);
67
+ try{
68
+ const sku=count%2?'SKU-713':'SKU-451',qty=2+(count%3),id='PED-'+(1001+AP.one('SELECT count(*) AS n FROM sales_orders').n),at=new Date().toISOString();
69
+ const product=AP.one('SELECT sale_price_cents FROM products WHERE sku=?',[sku]);
70
+ AP.tx(()=>{AP.exec({sql:'INSERT INTO sales_orders VALUES(?,?,?,?,?)',args:[id,'Canal digital','España',at,'pendiente'],audit:false});const inv=AP.one('SELECT on_hand,reserved FROM inventory WHERE sku=?',[sku]);const reserved=Math.min(inv.reserved+qty,inv.on_hand);AP.exec({sql:'UPDATE inventory SET reserved=?,updated_at=? WHERE sku=?',args:[reserved,at,sku],audit:false});AP.exec({sql:'INSERT INTO sales_order_lines VALUES(?,?,?,?,?)',args:[id,1,sku,qty,product.sale_price_cents],actor:'BETA',action:'pedido_generado',entity:'sales_order',id,detail:{sku,quantity:qty,source:'motor_local',reserved_before:inv.reserved,reserved}});});
71
+ await AP.persist();AP.render();AP.setAgentState('beta','Pedido registrado',false);
72
+ if(AP.one('SELECT needs_restock FROM stock_status WHERE sku=?',[sku]).needs_restock){
73
+ AP.setAgentState('alpha','Revisando pedido',true);
74
+ const pending=AP.one("SELECT p.id FROM purchase_proposals p JOIN approval_queue q ON q.proposal_id=p.id WHERE p.sku=? AND q.status IN ('pendiente','aprobada') LIMIT 1",[sku]);
75
+ if(!pending&&Date.now()-AP.lastModelCycleAt>=60000){AP.lastModelCycleAt=Date.now();await AP.ask(`Nuevo pedido ${id}: ${qty} unidades de ${sku}. Revisa stock y decide si hace falta propuesta de reposición.`,true);}
76
+ else AP.setAgentState('alpha',pending?'Propuesta en seguimiento':'En espera',false);
77
+ }
78
+ }finally{AP.cycleBusy=false;}
79
+ },
80
+ startCycle(){
81
+ if(AP.cycleTimer)clearInterval(AP.cycleTimer);
82
+ const el=document.getElementById('ap-cycle');
83
+ if(!AP.agentsOn){AP.cycleTimer=null;if(el)el.textContent='Motor de agentes detenido · sin consumo automático';return;}
84
+ AP.cycleTimer=setInterval(()=>AP.command({cmd:'pedido_generar',payload:{}}).catch(e=>AP.note('Motor de eventos: '+e.message,true)),AP.cycleIntervalMs);
85
+ if(el)el.textContent='Motor de eventos demo · cada 30 s con la pestaña visible';
86
+ },
87
+ toggleAgents(){AP.agentsOn=!AP.agentsOn;localStorage.setItem('adaptapro-agents',AP.agentsOn?'on':'off');AP.startCycle();AP.updateAgents();AP.note(AP.agentsOn?'Motor de agentes activado.':'Motor de agentes detenido. El chat sigue disponible.');},
88
+ updateAgents(){const b=document.getElementById('ap-agents');if(b){b.textContent=AP.agentsOn?'🤖 Agentes':'⏸ Agentes';b.setAttribute('aria-pressed',String(AP.agentsOn));b.title=AP.agentsOn?'Motor automático activo: analiza y propone con el modelo cuando hace falta. Deténlo para no consumir cuota; el chat sigue respondiendo si escribes.':'Motor automático detenido: ninguna llamada automática al modelo. El chat sigue respondiendo si escribes.';}const c=document.getElementById('ap-cycle');if(c)c.textContent=AP.agentsOn?'Motor de eventos demo · cada 30 s con la pestaña visible':'Motor de agentes detenido · sin consumo automático';},
89
+ };
90
+ globalThis.APCommands = APCommands;