Wip web interface

This commit is contained in:
Niels Geens
2025-04-17 22:09:15 +02:00
parent 813c6a3f8f
commit 023f6bba9a
13 changed files with 1040 additions and 1040 deletions

View File

@@ -180,4 +180,5 @@ class AutoApprover:
return
if (self._response_enabled and
self.agent.llms[self._llm_name] == LlmState.IDLE):
self.agent.approve_response(self._llm_name, self.agent.get_output(self._llm_name))
response = self.agent.response_buffer.get_text()
self.agent.approve_response()

View File

@@ -94,6 +94,8 @@ class Api:
"""Approve current buffer content"""
data = await request.json()
try:
response = data.get("response")
self._agent.response_buffer.set_text(response)
self._agent.approve_response()
return web.Response(status=200)
except ValueError as e:
@@ -147,10 +149,10 @@ class Api:
"""Get all LLMs and their current states."""
states = self._agent.llms
return web.Response(
text=json.dumps({
name: state.name
for name, state in states.items()
}),
text=json.dumps(
[{"name": name, "state": state.name}
for name, state in states.items()]
),
content_type="application/json"
)
@@ -234,10 +236,12 @@ class Api:
"""Get complete working memory state."""
entries = self._working_memory.get_entries()
return web.Response(
text=json.dumps([e.serialize() for e in entries]),
text=json.dumps({
"type": "memory_state",
"entries": [e.serialize() for e in entries]
}),
content_type="application/json"
)
async def _create_entry(self, request: web.Request) -> web.Response:
"""Create a new entry in working memory."""
data = await request.json()

View File

@@ -12,7 +12,7 @@ class ResponseWebSocket:
async def _handle_buffer_change(self, buffer):
await self._broadcast_message({
"buffer": buffer,
"response": buffer,
})
async def _broadcast_message(self, message):
@@ -32,7 +32,7 @@ class ResponseWebSocket:
try:
await ws.send_json({
"buffer": self._agent.response_buffer.get_text(),
"response": self._agent.response_buffer.get_text(),
})
async for msg in ws:

View File

@@ -22,7 +22,7 @@ class Websockets:
app.router.add_get("/ws/auto_approver", self._auto_approver_ws.handle_connection)
app.router.add_get("/ws/context", self._context_ws.handle_connection)
app.router.add_get("/ws/llm", self._llm_ws.handle_connection)
app.router.add_get("/ws/llms", self._llm_ws.handle_connection)
app.router.add_get("/ws/memory", self._memory_ws.handle_connection)
app.router.add_get("/ws/response", self._response_ws.handle_connection)
app.router.add_get("/ws/stdout", self._stdout_ws.handle_connection)

View File

@@ -132,6 +132,8 @@ class WebAgent(BaseAgent):
def approve_response(self) -> None:
"""Process approved response from specified LLM"""
if self.llms.get(llm_name) != LlmState.IDLE:
return
timestamp = datetime.now(timezone.utc)
self._iteration_logger.log_iteration(timestamp, self._context, self._response_buffer.get_text())
parse_result = self._parser.parse(timestamp, self._response_buffer.get_text())

View File

@@ -8,9 +8,8 @@ import { StandardEditor } from '@/components/editors/StandardEditor';
import { useWebSocket, WebSocketState } from '@/hooks/useWebSocket';
export const LlmState = {
NO_OUTPUT: 'NO_OUTPUT',
INFERENCE: 'INFERENCE',
OUTPUT: 'OUTPUT'
IDLE: 'IDLE',
INFERENCE: 'INFERENCE'
};
const App = () => {
@@ -18,8 +17,8 @@ const App = () => {
const [generatedContext, setGeneratedContext] = useState('');
const [modifiedContext, setModifiedContext] = useState('');
const [contextDirty, setContextDirty] = useState(false);
const [generatedResponses, setGeneratedResponses] = useState({});
const [modifiedResponses, setModifiedResponses] = useState({});
const [generatedResponse, setGeneratedResponse] = useState('');
const [modifiedResponse, setModifiedResponse] = useState('');
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
@@ -44,9 +43,9 @@ const App = () => {
// WebSocket connections
const wsRoot = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`
const llmWs = useWebSocket(`${wsRoot}/llm`);
const llmWs = useWebSocket(`${wsRoot}/llms`);
const contextWs = useWebSocket(`${wsRoot}/context`);
const tokenWs = useWebSocket(`${wsRoot}/token`);
const responseWs = useWebSocket(`${wsRoot}/response`);
const stdoutWs = useWebSocket(`${wsRoot}/stdout`);
const autoApproverWs = useWebSocket(`${wsRoot}/auto_approver`);
const memoryWs = useWebSocket(`${wsRoot}/memory`);
@@ -60,18 +59,6 @@ const App = () => {
...prevLlms,
[data.llm]: data.state
}));
if (data.state === LlmState.NO_OUTPUT) {
setGeneratedResponses(prev => {
const updated = { ...prev };
delete updated[data.llm];
return updated;
});
setModifiedResponses(prev => {
const updated = { ...prev };
delete updated[data.llm];
return updated;
});
}
});
}, []);
@@ -86,17 +73,11 @@ const App = () => {
});
}, []);
// Handle incoming tokens
// Handle response changes
useEffect(() => {
tokenWs.addMessageHandler((data) => {
setGeneratedResponses(prev => ({
...prev,
[data.llm]: (prev[data.llm] || '') + data.token
}));
setModifiedResponses(prev => ({
...prev,
[data.llm]: (prev[data.llm] || '') + data.token
}));
responseWs.addMessageHandler((data) => {
setModifiedResponse(data.response);
setGeneratedResponse(data.response);
});
}, []);
@@ -138,28 +119,33 @@ const App = () => {
};
const handleApprove = () => {
const state = llms[activeLlm];
if (state === LlmState.NO_OUTPUT) {
if (contextDirty) {
fetch('/api/response/approve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ response: modifiedResponse })
});
};
const handleInference = () => {
fetch('/api/context', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ context: modifiedContext })
});
}
fetch('/api/response', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ response: modifiedResponse })
});
fetch(`/api/inference/${activeLlm}`, {
method: 'POST',
});
} else if (state === LlmState.OUTPUT) {
fetch(`/api/approve/${activeLlm}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ response: modifiedResponses[activeLlm] })
});
}
};
const handleStop = () => {
// We still specify which LLM to stop inference for
fetch(`/api/inference/${activeLlm}/stop`, {
method: 'POST'
});
@@ -182,14 +168,22 @@ const App = () => {
const handleContextEdit = (context) => {
setContextDirty(true);
setGeneratedResponses({});
setModifiedResponses({});
// When context is edited, we need to reset all LLM states to IDLE
// since responses are no longer valid
const resetLlms = {};
for (const llm of Object.keys(llms)) {
resetLlms[llm] = LlmState.NO_OUTPUT;
resetLlms[llm] = LlmState.IDLE;
}
setLlms(resetLlms);
setModifiedContext(context);
// Reset response buffers
setGeneratedResponse('');
setModifiedResponse('');
};
const handleResponseEdit = (response) => {
setModifiedResponse(response);
};
const handleAutoApproverConfigChange = async (config) => {
@@ -246,37 +240,21 @@ const App = () => {
if (!response.ok) throw new Error('Failed to update entry');
};
// Update active tab on state change
useEffect(() => {
if (activeTab === Tabs.MEMORY) return;
const state = llms[activeLlm];
switch (state) {
case null:
break;
case LlmState.NO_OUTPUT:
setActiveTab(Tabs.CONTEXT);
setShowDiff(false);
setShowSidebar(false);
break;
default:
setActiveTab(Tabs.RESPONSE);
setShowDiff(false);
setShowSidebar(false);
break;
}
}, [llms[activeLlm]]);
// Determine active LLM's current state for UI feedback
const activeLlmState = activeLlm ? llms[activeLlm] : null;
// Track overall WebSocket connection state
useEffect(() => {
if (llmWs.wsState === WebSocketState.CONNECTED &&
contextWs.wsState === WebSocketState.CONNECTED &&
tokenWs.wsState === WebSocketState.CONNECTED &&
responseWs.wsState === WebSocketState.CONNECTED &&
stdoutWs.wsState === WebSocketState.CONNECTED &&
autoApproverWs.wsState === WebSocketState.CONNECTED &&
memoryWs.wsState === WebSocketState.CONNECTED) {
setWsState(WebSocketState.CONNECTED);
} else if (llmWs.wsState === WebSocketState.CONNECTING ||
contextWs.wsState === WebSocketState.CONNECTING ||
tokenWs.wsState === WebSocketState.CONNECTING ||
responseWs.wsState === WebSocketState.CONNECTING ||
stdoutWs.wsState === WebSocketState.CONNECTING ||
autoApproverWs.wsState === WebSocketState.CONNECTING ||
memoryWs.wsState === WebSocketState.CONNECTING) {
@@ -284,13 +262,13 @@ const App = () => {
} else {
setWsState(WebSocketState.DISCONNECTED);
}
}, [llmWs.wsState, contextWs.wsState, tokenWs.wsState, stdoutWs.wsState, autoApproverWs.wsState, memoryWs.wsState]);
}, [llmWs.wsState, contextWs.wsState, responseWs.wsState, stdoutWs.wsState, autoApproverWs.wsState, memoryWs.wsState]);
return (
<div className="h-screen bg-gray-100 flex flex-col overflow-hidden">
<Header
wsState={wsState}
agentState={llms[activeLlm]}
agentState={activeLlmState}
activeTab={activeTab}
setActiveTab={setActiveTab}
/>
@@ -309,12 +287,9 @@ const App = () => {
{activeTab === Tabs.RESPONSE && (
<ContentEditor
showDiff={showDiff}
originalContent={generatedResponses[activeLlm] || ''}
modifiedContent={modifiedResponses[activeLlm] || ''}
onChange={value => setModifiedResponses(prev => ({
...prev,
[activeLlm]: value
}))}
originalContent={generatedResponse}
modifiedContent={modifiedResponse}
onChange={handleResponseEdit}
/>
)}
{activeTab === Tabs.INPUT && (
@@ -347,14 +322,15 @@ const App = () => {
showSidebar={showSidebar}
llms={llms}
activeLlm={activeLlm}
llmState={llms[activeLlm]}
llmState={activeLlmState}
showDiff={showDiff}
autoScroll={autoScroll}
onAutoScrollChange={(enabled) => setAutoScroll(enabled)}
onAutoScrollChange={setAutoScroll}
input={input}
output={output}
autoApproverConfig={autoApproverConfig}
onApprove={handleApprove}
onInference={handleInference}
onStop={handleStop}
onToggleDiff={() => setShowDiff(!showDiff)}
onSendInput={handleSendInput}

View File

@@ -7,7 +7,6 @@ export const Tabs = {
INPUT: 'INPUT',
OUTPUT: 'OUTPUT',
MEMORY: 'MEMORY'
};
export const Header = ({
@@ -31,9 +30,11 @@ export const Header = ({
}`}>
{wsState}
</div>
{agentState && (
<div className="px-3 py-1 rounded-full bg-blue-100 text-blue-800 text-sm">
{agentState}
</div>
)}
</div>
</div>
<div className="border-t">
@@ -48,7 +49,7 @@ export const Header = ({
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
{tab.charAt(0).toUpperCase() + tab.slice(1).toLowerCase()}
</button>
))}
</div>

View File

@@ -16,6 +16,7 @@ export const Sidebar = ({
output,
autoApproverConfig,
onApprove,
onInference,
onStop,
onToggleDiff,
onSendInput,
@@ -61,15 +62,27 @@ export const Sidebar = ({
/>
</div>
{llmState === LlmState.INFERENCE ? (
<Button
onClick={llmState === LlmState.INFERENCE ? onStop : onApprove}
onClick={onStop}
className="w-full"
>
{
llmState === LlmState.NO_OUTPUT ? 'Inference' :
llmState === LlmState.INFERENCE ? 'Stop' :
'Approve'
}
Stop
</Button>
) : (
<Button
onClick={onInference}
className="w-full"
>
Start Inference
</Button>
)}
<Button
onClick={onApprove}
className="w-full"
>
Approve Response
</Button>
<Button onClick={onSendInput} disabled={!input.trim()} className="w-full">
@@ -136,7 +149,7 @@ export const Sidebar = ({
</div>
<div className="space-y-1">
<label className="text-sm">Auto-Approve LLM</label>
<label className="text-sm">LLM for Inference</label>
<Select
value={autoApproverConfig.llm_name}
onValueChange={(llm) => onAutoApproverConfigChange({
@@ -153,6 +166,9 @@ export const Sidebar = ({
))}
</SelectContent>
</Select>
<p className="text-xs text-gray-500 mt-1">
This selects which model will generate content during auto-approval.
</p>
</div>
</div>
</div>