feat(emma): deploy browser-based Emma Local operator POC
This commit is contained in:
parent
0b40d556cd
commit
8f1c501651
|
|
@ -10,6 +10,7 @@ const READ_ONLY_TOOLS = new Set([
|
||||||
'get_build_status',
|
'get_build_status',
|
||||||
'get_state',
|
'get_state',
|
||||||
'get_telemetry',
|
'get_telemetry',
|
||||||
|
'run_emma',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export class AgentNotConfiguredError extends Error {
|
export class AgentNotConfiguredError extends Error {
|
||||||
|
|
|
||||||
27
opax-web/backend/capabilityRegistry.js
Normal file
27
opax-web/backend/capabilityRegistry.js
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
export const capabilities = {
|
||||||
|
get_health: { type: 'read' },
|
||||||
|
get_build_status: { type: 'read' },
|
||||||
|
get_state: { type: 'read' },
|
||||||
|
get_telemetry: { type: 'read' },
|
||||||
|
list_commits: { type: 'read' },
|
||||||
|
get_file: { type: 'read' },
|
||||||
|
list_customers: { type: 'read' },
|
||||||
|
get_billing_summary: { type: 'read' },
|
||||||
|
get_billing_forecast: { type: 'read' },
|
||||||
|
get_billing_anomalies: { type: 'read' },
|
||||||
|
get_billing_budget: { type: 'read' },
|
||||||
|
list_workspace_users: { type: 'read' },
|
||||||
|
list_user_aliases: { type: 'read' },
|
||||||
|
list_calendar_events: { type: 'read' },
|
||||||
|
run_emma: { type: 'read' },
|
||||||
|
trigger_build: { type: 'proposal' },
|
||||||
|
commit_and_push_files: { type: 'proposal' },
|
||||||
|
create_a2h2a_ticket: { type: 'proposal' },
|
||||||
|
send_email: { type: 'proposal' },
|
||||||
|
send_sms: { type: 'proposal' },
|
||||||
|
send_webhook: { type: 'proposal' },
|
||||||
|
create_invite: { type: 'proposal' },
|
||||||
|
create_email_alias: { type: 'proposal' },
|
||||||
|
delete_email_alias: { type: 'proposal' },
|
||||||
|
set_billing_budget: { type: 'proposal' },
|
||||||
|
};
|
||||||
|
|
@ -67,4 +67,38 @@ router.get('/approvals', (req, res) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/emma/chat', async (req, res) => {
|
||||||
|
const { message, session_id, ticket_number } = req.body;
|
||||||
|
|
||||||
|
if (!message || typeof message !== 'string' || message.length > 4096) {
|
||||||
|
return res.status(400).json({ status: 'error', message: 'Invalid message.'});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ticket_number && (typeof ticket_number !== 'number' || !Number.isInteger(ticket_number) || ticket_number <= 0)) {
|
||||||
|
return res.status(400).json({ status: 'error', message: 'Invalid ticket number.'});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const emmaData = await callAgent('run_emma', { prompt: message, history: [] });
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
status: 'success',
|
||||||
|
reply: emmaData.response,
|
||||||
|
session_id,
|
||||||
|
ticket_number,
|
||||||
|
model_label: 'Emma Local',
|
||||||
|
message: 'Message processed by Emma.',
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ status: 'error', message: 'Could not connect to Emma.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
import { capabilities } from '../capabilityRegistry.js';
|
||||||
|
|
||||||
|
router.get('/capabilities', (req, res) => {
|
||||||
|
res.json(capabilities);
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ const sm = session({
|
||||||
const io = new Server(http, { cors: { origin: front, credentials: true } });
|
const io = new Server(http, { cors: { origin: front, credentials: true } });
|
||||||
|
|
||||||
app.use(cors({ origin: front, credentials: true }));
|
app.use(cors({ origin: front, credentials: true }));
|
||||||
|
app.use(express.json());
|
||||||
app.use(sm);
|
app.use(sm);
|
||||||
app.use(express.static(frontendDistPath));
|
app.use(express.static(frontendDistPath));
|
||||||
io.use((s, n) => sm(s.request, {}, n));
|
io.use((s, n) => sm(s.request, {}, n));
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,73 @@
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
const AgentConsoleView = () => {
|
const AgentConsoleView = () => {
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
const [transcript, setTranscript] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [ticketNumber, setTicketNumber] = useState(null);
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
if (!message.trim()) return;
|
||||||
|
|
||||||
|
const newMessage = { role: 'user', content: message };
|
||||||
|
setTranscript(prev => [...prev, newMessage]);
|
||||||
|
setMessage('');
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/console/emma/chat', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({ message, ticket_number: ticketNumber }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('Emma is unavailable.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
const emmaMessage = { role: 'assistant', content: data.reply };
|
||||||
|
setTranscript(prev => [...prev, emmaMessage]);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="console-card">
|
<div className="console-card">
|
||||||
<h2>Agent Console</h2>
|
<h2>Emma Local</h2>
|
||||||
<div className="workflow-diagram">
|
<div className="agent-info">
|
||||||
<span>Draft</span> → <span>Plan</span> → <span>Scope Approval</span> → <span>Implementation</span> → <span>Diff Approval</span> → <span>Validation</span> → <span>Deployment Approval</span> → <span>Deploy</span>
|
<span>Local model · server-side memory enabled</span>
|
||||||
|
{ticketNumber && <span>Working context: Gitea #{ticketNumber}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="transcript">
|
||||||
|
{transcript.map((msg, index) => (
|
||||||
|
<div key={index} className={`message ${msg.role}`}>
|
||||||
|
<strong>{msg.role}: </strong>{msg.content}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{loading && <div className="message assistant">...</div>}
|
||||||
|
{error && <div className="message error">{error}</div>}
|
||||||
|
</div>
|
||||||
|
<div className="chat-input">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
|
||||||
|
placeholder="Chat with Emma..."
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<button onClick={handleSend} disabled={loading}>
|
||||||
|
{loading ? 'Sending...' : 'Send'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="unavailable-message">Ticket-bound agent execution is not configured yet.</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
49
opax-web/frontend/src/components/CapabilityPanel.tsx
Normal file
49
opax-web/frontend/src/components/CapabilityPanel.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const CapabilityPanel = () => {
|
||||||
|
const [capabilities, setCapabilities] = useState({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchCapabilities = async () => {
|
||||||
|
const res = await fetch('/api/console/capabilities');
|
||||||
|
const data = await res.json();
|
||||||
|
setCapabilities(data);
|
||||||
|
};
|
||||||
|
fetchCapabilities();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const capabilityGroups = {
|
||||||
|
'Platform Status': ['get_health', 'get_build_status', 'get_state', 'get_telemetry'],
|
||||||
|
'Git and Gitea': ['list_commits', 'get_file'],
|
||||||
|
'Tickets and Work Queue': [],
|
||||||
|
'Code Agents': ['run_emma'],
|
||||||
|
'Tests and Validation': [],
|
||||||
|
'Builds and Deployments': ['trigger_build', 'commit_and_push_files'],
|
||||||
|
'Cloud Run and Infrastructure': [],
|
||||||
|
'Logs and Telemetry': [],
|
||||||
|
'Billing and CostGuard': ['get_billing_summary', 'get_billing_forecast', 'get_billing_anomalies', 'get_billing_budget', 'set_billing_budget'],
|
||||||
|
'Customers and Workspace': ['list_customers', 'list_workspace_users', 'create_invite'],
|
||||||
|
'Notifications': ['send_email', 'send_sms', 'send_webhook'],
|
||||||
|
'Memory and Knowledge': [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="capability-panel">
|
||||||
|
<h3>Capabilities</h3>
|
||||||
|
{Object.entries(capabilityGroups).map(([group, capabilityKeys]) => (
|
||||||
|
<div key={group} className="capability-group">
|
||||||
|
<h4>{group}</h4>
|
||||||
|
<ul>
|
||||||
|
{capabilityKeys.map(key => {
|
||||||
|
const capability = capabilities[key];
|
||||||
|
if (!capability) return null;
|
||||||
|
return <li key={key} className={capability.type}>{key}</li>
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CapabilityPanel;
|
||||||
|
|
@ -4,6 +4,7 @@ import ProjectRegistryView from '../components/ProjectRegistryView';
|
||||||
import WorkQueueView from '../components/WorkQueueView';
|
import WorkQueueView from '../components/WorkQueueView';
|
||||||
import ApprovalsView from '../components/ApprovalsView';
|
import ApprovalsView from '../components/ApprovalsView';
|
||||||
import AgentConsoleView from '../components/AgentConsoleView';
|
import AgentConsoleView from '../components/AgentConsoleView';
|
||||||
|
import CapabilityPanel from '../components/CapabilityPanel';
|
||||||
|
|
||||||
// --- Types for Safe Data Handling ---
|
// --- Types for Safe Data Handling ---
|
||||||
type ConsoleStatus = 'success' | 'unavailable';
|
type ConsoleStatus = 'success' | 'unavailable';
|
||||||
|
|
@ -130,6 +131,7 @@ const OperationsConsole = () => {
|
||||||
<WorkQueueView />
|
<WorkQueueView />
|
||||||
<ApprovalsView />
|
<ApprovalsView />
|
||||||
<AgentConsoleView />
|
<AgentConsoleView />
|
||||||
|
<CapabilityPanel />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user