mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-07-09 22:04:21 +00:00
First commit
This commit is contained in:
364
apps/agent_frontend/app.js
Normal file
364
apps/agent_frontend/app.js
Normal file
@@ -0,0 +1,364 @@
|
||||
const chat=document.getElementById('chat');
|
||||
const form=document.getElementById('form');
|
||||
let eventSource=null;
|
||||
let currentSessionId = null;
|
||||
|
||||
function add(role,text){
|
||||
const d=document.createElement('div');
|
||||
d.className='msg '+role;
|
||||
d.textContent=text;
|
||||
chat.appendChild(d);
|
||||
chat.scrollTop=chat.scrollHeight;
|
||||
}
|
||||
function status(text){const el=document.getElementById('status'); if(el) el.textContent=text;}
|
||||
function val(id){return (document.getElementById(id)?.value || '').trim();}
|
||||
function uuid(){return crypto.randomUUID();}
|
||||
|
||||
function buildBusinessContext(session, messageId){
|
||||
return {
|
||||
customer_key: val('customerKey') || null,
|
||||
contract_key: val('contractKey') || null,
|
||||
interaction_key: val('interactionKey') || messageId,
|
||||
account_key: val('accountKey') || null,
|
||||
resource_key: val('resourceKey') || null,
|
||||
session_key: session || null,
|
||||
metadata: {frontend: 'agent_frontend', version: 'business-context-v2'}
|
||||
};
|
||||
}
|
||||
|
||||
function syncDomainAliases(payload, businessContext){
|
||||
const agent=val('agent');
|
||||
if(agent === 'retail_orders'){
|
||||
payload.customer_id = businessContext.customer_key;
|
||||
payload.order_id = businessContext.contract_key;
|
||||
} else {
|
||||
payload.msisdn = businessContext.customer_key;
|
||||
payload.invoice_id = businessContext.contract_key;
|
||||
payload.ura_call_id = businessContext.interaction_key;
|
||||
payload.asset_id = businessContext.resource_key;
|
||||
}
|
||||
}
|
||||
|
||||
function adicionarMensagem(role, text) {
|
||||
const chat =
|
||||
document.getElementById("chat") ||
|
||||
document.getElementById("messages") ||
|
||||
document.querySelector(".chat") ||
|
||||
document.querySelector(".messages") ||
|
||||
document.querySelector("[data-chat]");
|
||||
|
||||
if (!chat) {
|
||||
console.error("Não encontrei o container do chat no HTML.");
|
||||
console.log("Mensagem que seria exibida:", role, text);
|
||||
return;
|
||||
}
|
||||
|
||||
const div = document.createElement("div");
|
||||
|
||||
if (role === "user") {
|
||||
div.className = "msg user chat-bubble--user";
|
||||
} else {
|
||||
div.className = "msg assistant chat-bubble--agent";
|
||||
}
|
||||
|
||||
div.textContent = text || "";
|
||||
|
||||
chat.appendChild(div);
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
}
|
||||
|
||||
function abrirSSE(sessionId) {
|
||||
if (!sessionId) {
|
||||
console.error("Não vou abrir SSE sem sessionId.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
|
||||
const url = `${backend}/gateway/events/${sessionId}`;
|
||||
|
||||
eventSource = new EventSource(url);
|
||||
|
||||
eventSource.onopen = () => {
|
||||
console.log("SSE OPEN");
|
||||
};
|
||||
|
||||
eventSource.onerror = (err) => {
|
||||
console.error("SSE ERROR:", err);
|
||||
};
|
||||
|
||||
const eventos = [
|
||||
"connected",
|
||||
"waiting",
|
||||
"backend.selected",
|
||||
"flow.start",
|
||||
"workflow.started",
|
||||
"message.responded",
|
||||
"workflow.completed",
|
||||
"flow.end",
|
||||
"error"
|
||||
];
|
||||
|
||||
for (const nome of eventos) {
|
||||
eventSource.addEventListener(nome, (event) => {
|
||||
|
||||
if (nome === "message.responded") {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
const text =
|
||||
data.text ||
|
||||
data.message ||
|
||||
data.response ||
|
||||
data.content ||
|
||||
data.output ||
|
||||
event.data;
|
||||
|
||||
adicionarMensagem("assistant", text);
|
||||
} catch {
|
||||
adicionarMensagem("assistant", event.data);
|
||||
}
|
||||
}
|
||||
|
||||
if (nome === "error") {
|
||||
adicionarMensagem("assistant", `Erro SSE: ${event.data}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSessionId(value) {
|
||||
if (!value) return uuid();
|
||||
|
||||
const parts = value.split(":");
|
||||
return parts[parts.length - 1]; // mantém só o UUID final
|
||||
}
|
||||
|
||||
function connectSSE(backend, sessionId) {
|
||||
if (!sessionId) {
|
||||
console.warn("SSE não aberto: sessionId ausente.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!backend) {
|
||||
console.warn("SSE não aberto: backend ausente.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventSource) {
|
||||
console.log("Fechando SSE anterior:", eventSource.url);
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
|
||||
const url = `${backend.replace(/\/$/, "")}/gateway/events/${encodeURIComponent(sessionId)}`;
|
||||
|
||||
console.log("Abrindo SSE:", url);
|
||||
|
||||
eventSource = new EventSource(url);
|
||||
eventSource._sessionId = sessionId;
|
||||
|
||||
eventSource.onopen = () => {
|
||||
console.log("SSE OPEN:", url);
|
||||
status("SSE conectado");
|
||||
};
|
||||
|
||||
eventSource.onerror = (err) => {
|
||||
console.error("SSE ERROR raw:", err);
|
||||
console.error("SSE readyState:", eventSource?.readyState);
|
||||
console.error("SSE url:", eventSource?.url);
|
||||
|
||||
if (eventSource?.readyState === EventSource.CONNECTING) {
|
||||
status("SSE aguardando/reconectando");
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventSource?.readyState === EventSource.CLOSED) {
|
||||
status("SSE fechado");
|
||||
return;
|
||||
}
|
||||
|
||||
status("SSE com erro");
|
||||
};
|
||||
|
||||
eventSource.addEventListener("connected", (event) => {
|
||||
console.log("SSE connected:", event.data);
|
||||
status("SSE conectado");
|
||||
});
|
||||
|
||||
eventSource.addEventListener("waiting", (event) => {
|
||||
console.log("SSE waiting:", event.data);
|
||||
status("SSE aguardando backend");
|
||||
});
|
||||
|
||||
eventSource.addEventListener("backend.selected", (event) => {
|
||||
console.log("SSE backend.selected:", event.data);
|
||||
status("Backend selecionado");
|
||||
});
|
||||
|
||||
eventSource.addEventListener("flow.start", (event) => {
|
||||
console.log("SSE flow.start:", event.data);
|
||||
status("Fluxo iniciado");
|
||||
});
|
||||
|
||||
eventSource.addEventListener("workflow.started", (event) => {
|
||||
console.log("SSE workflow.started:", event.data);
|
||||
status("Workflow em execução");
|
||||
});
|
||||
|
||||
eventSource.addEventListener("session.upserted", (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.business_context) {
|
||||
console.debug("business_context", data.business_context);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Não consegui interpretar session.upserted:", event.data);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("message.responded", (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
const text =
|
||||
data.text ||
|
||||
data.message ||
|
||||
data.response ||
|
||||
data.content ||
|
||||
data.output ||
|
||||
event.data;
|
||||
|
||||
if (text) {
|
||||
add("assistant", text);
|
||||
}
|
||||
|
||||
if (data.metadata?.business_context) {
|
||||
console.debug("metadata.business_context", data.metadata.business_context);
|
||||
}
|
||||
|
||||
status("Resposta recebida");
|
||||
} catch (e) {
|
||||
add("assistant", event.data);
|
||||
status("Resposta recebida");
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("workflow.completed", (event) => {
|
||||
console.log("SSE workflow.completed:", event.data);
|
||||
status("Workflow concluído");
|
||||
});
|
||||
|
||||
eventSource.addEventListener("flow.end", (event) => {
|
||||
console.log("SSE flow.end:", event.data);
|
||||
status("Fluxo finalizado");
|
||||
});
|
||||
|
||||
// Use um nome diferente de "error" para erro enviado pelo servidor.
|
||||
// "error" é reservado/conflituoso com erro nativo do EventSource.
|
||||
eventSource.addEventListener("server.error", (event) => {
|
||||
console.error("SSE server.error:", event.data);
|
||||
add("assistant", `Erro SSE: ${event.data || "erro informado pelo servidor"}`);
|
||||
status("Erro no fluxo SSE");
|
||||
});
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const input = document.getElementById('message');
|
||||
const text = input.value.trim();
|
||||
|
||||
if (!text) return;
|
||||
|
||||
adicionarMensagem('user', text);
|
||||
input.value = '';
|
||||
|
||||
const backend = val('backend').replace(/\/$/, '');
|
||||
const channel = val('channel');
|
||||
// const session = val('session') || uuid();
|
||||
const session = normalizeSessionId(val('session'));
|
||||
const messageId = uuid();
|
||||
const tenantId = val('tenant') || 'default';
|
||||
const agentId = val('agent') || 'telecom_contas';
|
||||
|
||||
document.getElementById('session').value = session;
|
||||
|
||||
const businessContext = buildBusinessContext(session, messageId);
|
||||
|
||||
const commonContext = {
|
||||
channel_id: 'browser',
|
||||
tenant_id: tenantId,
|
||||
agent_id: agentId,
|
||||
business_context: businessContext
|
||||
};
|
||||
|
||||
const payload = channel === 'voice'
|
||||
? {
|
||||
transcript: text,
|
||||
session_id: session,
|
||||
ani: businessContext.customer_key,
|
||||
message_id: messageId,
|
||||
tenant_id: tenantId,
|
||||
agent_id: agentId,
|
||||
context: commonContext
|
||||
}
|
||||
: {
|
||||
message: text,
|
||||
text: text,
|
||||
session_id: session,
|
||||
user_id: businessContext.customer_key || 'web-user',
|
||||
message_id: messageId,
|
||||
tenant_id: tenantId,
|
||||
agent_id: agentId,
|
||||
context: commonContext
|
||||
};
|
||||
|
||||
syncDomainAliases(payload, businessContext);
|
||||
|
||||
try {
|
||||
status('Enviando mensagem');
|
||||
|
||||
const res = await fetch(`${backend}/gateway/message`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
channel,
|
||||
tenant_id: tenantId,
|
||||
agent_id: agentId,
|
||||
payload
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
const returnedSessionId = data.session_id || session;
|
||||
currentSessionId = returnedSessionId;
|
||||
document.getElementById('session').value = returnedSessionId;
|
||||
|
||||
const resposta =
|
||||
data.text ||
|
||||
data.speak ||
|
||||
data.message ||
|
||||
data.response ||
|
||||
data.content ||
|
||||
data.output ||
|
||||
JSON.stringify(data);
|
||||
|
||||
adicionarMensagem('assistant', resposta);
|
||||
status('Resposta recebida');
|
||||
|
||||
} catch (err) {
|
||||
adicionarMensagem('assistant', `Erro ao chamar backend: ${err.message}`);
|
||||
status('Erro de conexão');
|
||||
}
|
||||
});
|
||||
49
apps/agent_frontend/index.html
Normal file
49
apps/agent_frontend/index.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<!doctype html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>AI Agent Frontend</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="app">
|
||||
<header>
|
||||
<h1>AI Agent Platform</h1>
|
||||
<p>Frontend independente com chaves de conversa propagadas até o framework e MCP Server.</p>
|
||||
</header>
|
||||
|
||||
<section class="config">
|
||||
<label>Backend URL <input id="backend" value="http://localhost:8000" /></label>
|
||||
<label>Canal
|
||||
<select id="channel"><option value="web">web</option><option value="whatsapp">whatsapp</option><option value="voice">voice</option></select>
|
||||
</label>
|
||||
<label>Tenant <input id="tenant" value="default" /></label>
|
||||
<label>Agent
|
||||
<select id="agent">
|
||||
<option value="telecom_contas">telecom_contas</option>
|
||||
<option value="retail_orders">retail_orders</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Session ID <input id="session" placeholder="gerado automaticamente" /></label>
|
||||
<label><input id="useSse" type="checkbox" checked /> Usar SSE</label>
|
||||
<span id="status">SSE aguardando</span>
|
||||
</section>
|
||||
|
||||
<section class="config identity">
|
||||
<label>customer_key <input id="customerKey" value="11999999999" /></label>
|
||||
<label>contract_key <input id="contractKey" value="3000131180" /></label>
|
||||
<label>interaction_key <input id="interactionKey" placeholder="URA/call/message id" /></label>
|
||||
<label>resource_key <input id="resourceKey" placeholder="asset/product/resource" /></label>
|
||||
<label>account_key <input id="accountKey" placeholder="billing account/customer account" /></label>
|
||||
</section>
|
||||
|
||||
<section id="chat" class="chat"></section>
|
||||
<form id="form">
|
||||
<input id="message" placeholder="Digite sua mensagem..." autocomplete="off" />
|
||||
<button>Enviar</button>
|
||||
</form>
|
||||
</main>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1
apps/agent_frontend/styles.css
Normal file
1
apps/agent_frontend/styles.css
Normal file
@@ -0,0 +1 @@
|
||||
body{font-family:system-ui,Arial,sans-serif;background:#f6f7fb;margin:0}.app{max-width:900px;margin:0 auto;padding:24px}header{margin-bottom:16px}.config{display:grid;grid-template-columns:1fr 160px 1fr;gap:12px;margin-bottom:16px}.config input,.config select,#message{width:100%;padding:10px;border:1px solid #ccc;border-radius:8px}.chat{background:#fff;border:1px solid #ddd;border-radius:12px;min-height:420px;padding:16px;overflow:auto}.msg{padding:10px 12px;border-radius:10px;margin:8px 0;max-width:75%}.user{background:#e8f0ff;margin-left:auto}.assistant{background:#f0f0f0}form{display:flex;gap:8px;margin-top:12px}button{padding:10px 16px;border:0;border-radius:8px;background:#111;color:#fff;cursor:pointer}
|
||||
Reference in New Issue
Block a user