- Added `trillium.py` for searching and creating notes with Trillium's ETAPI. - Implemented `search_notes` and `create_note` functions with appropriate error handling and validation. feat: Add web search functionality using DuckDuckGo - Introduced `web_search.py` for performing web searches without API keys. - Implemented `search_web` function with result handling and validation. feat: Create provider-agnostic function caller for iterative tool calling - Developed `function_caller.py` to manage LLM interactions with tools. - Implemented iterative calling logic with error handling and tool execution. feat: Establish a tool registry for managing available tools - Created `registry.py` to define and manage tool availability and execution. - Integrated feature flags for enabling/disabling tools based on environment variables. feat: Implement event streaming for tool calling processes - Added `stream_events.py` to manage Server-Sent Events (SSE) for tool calling. - Enabled real-time updates during tool execution for enhanced user experience. test: Add tests for tool calling system components - Created `test_tools.py` to validate functionality of code execution, web search, and tool registry. - Implemented asynchronous tests to ensure proper execution and result handling. chore: Add Dockerfile for sandbox environment setup - Created `Dockerfile` to set up a Python environment with necessary dependencies for code execution. chore: Add debug regex script for testing XML parsing - Introduced `debug_regex.py` to validate regex patterns against XML tool calls. chore: Add HTML template for displaying thinking stream events - Created `test_thinking_stream.html` for visualizing tool calling events in a user-friendly format. test: Add tests for OllamaAdapter XML parsing - Developed `test_ollama_parser.py` to validate XML parsing with various test cases, including malformed XML.
363 lines
10 KiB
HTML
363 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>🧠 Thinking Stream</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: #0d0d0d;
|
|
color: #e0e0e0;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.header {
|
|
background: #1a1a1a;
|
|
padding: 15px 20px;
|
|
border-bottom: 2px solid #333;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
background: #666;
|
|
}
|
|
|
|
.status-dot.connected {
|
|
background: #90ee90;
|
|
box-shadow: 0 0 10px #90ee90;
|
|
}
|
|
|
|
.status-dot.disconnected {
|
|
background: #ff6b6b;
|
|
}
|
|
|
|
.events-container {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
.event {
|
|
margin-bottom: 12px;
|
|
padding: 10px 15px;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
font-family: 'Courier New', monospace;
|
|
animation: slideIn 0.3s ease-out;
|
|
border-left: 3px solid;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(-20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
.event-connected {
|
|
background: #1a2a1a;
|
|
border-color: #4a7c59;
|
|
color: #90ee90;
|
|
}
|
|
|
|
.event-thinking {
|
|
background: #1a3a1a;
|
|
border-color: #5a9c69;
|
|
color: #a0f0a0;
|
|
}
|
|
|
|
.event-tool_call {
|
|
background: #3a2a1a;
|
|
border-color: #d97706;
|
|
color: #fbbf24;
|
|
}
|
|
|
|
.event-tool_result {
|
|
background: #1a2a3a;
|
|
border-color: #0ea5e9;
|
|
color: #7dd3fc;
|
|
}
|
|
|
|
.event-done {
|
|
background: #2a1a3a;
|
|
border-color: #a855f7;
|
|
color: #e9d5ff;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.event-error {
|
|
background: #3a1a1a;
|
|
border-color: #dc2626;
|
|
color: #fca5a5;
|
|
}
|
|
|
|
.event-icon {
|
|
display: inline-block;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.event-details {
|
|
font-size: 12px;
|
|
color: #999;
|
|
margin-top: 5px;
|
|
padding-left: 25px;
|
|
}
|
|
|
|
.footer {
|
|
background: #1a1a1a;
|
|
padding: 10px 20px;
|
|
border-top: 1px solid #333;
|
|
text-align: center;
|
|
font-size: 12px;
|
|
color: #666;
|
|
}
|
|
|
|
.clear-btn {
|
|
background: #333;
|
|
border: 1px solid #444;
|
|
color: #e0e0e0;
|
|
padding: 6px 12px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.clear-btn:hover {
|
|
background: #444;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
color: #666;
|
|
}
|
|
|
|
.empty-state-icon {
|
|
font-size: 48px;
|
|
margin-bottom: 20px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>🧠 Thinking Stream</h1>
|
|
<div class="status">
|
|
<div class="status-dot" id="statusDot"></div>
|
|
<span id="statusText">Connecting...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="events-container" id="events">
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🤔</div>
|
|
<p>Waiting for thinking events...</p>
|
|
<p style="font-size: 12px; margin-top: 10px;">Events will appear here when Lyra uses tools</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<button class="clear-btn" onclick="clearEvents()">Clear Events</button>
|
|
<span style="margin: 0 20px;">|</span>
|
|
<span id="sessionInfo">Session: <span id="sessionId">-</span></span>
|
|
</div>
|
|
|
|
<script>
|
|
console.log('🧠 Thinking stream page loaded!');
|
|
|
|
// Get session ID from URL
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const SESSION_ID = urlParams.get('session');
|
|
const CORTEX_BASE = "http://10.0.0.41:7081"; // Direct to cortex
|
|
|
|
console.log('Session ID:', SESSION_ID);
|
|
console.log('Cortex base:', CORTEX_BASE);
|
|
|
|
// Declare variables first
|
|
let eventSource = null;
|
|
let eventCount = 0;
|
|
|
|
if (!SESSION_ID) {
|
|
document.getElementById('events').innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">⚠️</div>
|
|
<p>No session ID provided</p>
|
|
<p style="font-size: 12px; margin-top: 10px;">Please open this from the main chat interface</p>
|
|
</div>
|
|
`;
|
|
} else {
|
|
document.getElementById('sessionId').textContent = SESSION_ID;
|
|
connectStream();
|
|
}
|
|
|
|
function connectStream() {
|
|
if (eventSource) {
|
|
eventSource.close();
|
|
}
|
|
|
|
const url = `${CORTEX_BASE}/stream/thinking/${SESSION_ID}`;
|
|
console.log('Connecting to:', url);
|
|
|
|
eventSource = new EventSource(url);
|
|
|
|
eventSource.onopen = () => {
|
|
console.log('EventSource onopen fired');
|
|
updateStatus(true, 'Connected');
|
|
};
|
|
|
|
eventSource.onmessage = (event) => {
|
|
console.log('Received message:', event.data);
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
// Update status to connected when first message arrives
|
|
if (data.type === 'connected') {
|
|
updateStatus(true, 'Connected');
|
|
}
|
|
addEvent(data);
|
|
} catch (e) {
|
|
console.error('Failed to parse event:', e, event.data);
|
|
}
|
|
};
|
|
|
|
eventSource.onerror = (error) => {
|
|
console.error('Stream error:', error, 'readyState:', eventSource.readyState);
|
|
updateStatus(false, 'Disconnected');
|
|
|
|
// Try to reconnect after 2 seconds
|
|
setTimeout(() => {
|
|
if (eventSource.readyState === EventSource.CLOSED) {
|
|
console.log('Attempting to reconnect...');
|
|
connectStream();
|
|
}
|
|
}, 2000);
|
|
};
|
|
}
|
|
|
|
function updateStatus(connected, text) {
|
|
const dot = document.getElementById('statusDot');
|
|
const statusText = document.getElementById('statusText');
|
|
|
|
dot.className = 'status-dot ' + (connected ? 'connected' : 'disconnected');
|
|
statusText.textContent = text;
|
|
}
|
|
|
|
function addEvent(event) {
|
|
const container = document.getElementById('events');
|
|
|
|
// Remove empty state if present
|
|
if (eventCount === 0) {
|
|
container.innerHTML = '';
|
|
}
|
|
|
|
const eventDiv = document.createElement('div');
|
|
eventDiv.className = `event event-${event.type}`;
|
|
|
|
let icon = '';
|
|
let message = '';
|
|
let details = '';
|
|
|
|
switch (event.type) {
|
|
case 'connected':
|
|
icon = '✓';
|
|
message = 'Stream connected';
|
|
details = `Session: ${event.session_id}`;
|
|
break;
|
|
|
|
case 'thinking':
|
|
icon = '🤔';
|
|
message = event.data.message;
|
|
break;
|
|
|
|
case 'tool_call':
|
|
icon = '🔧';
|
|
message = event.data.message;
|
|
details = JSON.stringify(event.data.args, null, 2);
|
|
break;
|
|
|
|
case 'tool_result':
|
|
icon = '📊';
|
|
message = event.data.message;
|
|
if (event.data.result && event.data.result.stdout) {
|
|
details = `stdout: ${event.data.result.stdout}`;
|
|
}
|
|
break;
|
|
|
|
case 'done':
|
|
icon = '✅';
|
|
message = event.data.message;
|
|
details = event.data.final_answer;
|
|
break;
|
|
|
|
case 'error':
|
|
icon = '❌';
|
|
message = event.data.message;
|
|
break;
|
|
|
|
default:
|
|
icon = '•';
|
|
message = JSON.stringify(event.data);
|
|
}
|
|
|
|
eventDiv.innerHTML = `
|
|
<span class="event-icon">${icon}</span>
|
|
<span>${message}</span>
|
|
${details ? `<div class="event-details">${details}</div>` : ''}
|
|
`;
|
|
|
|
container.appendChild(eventDiv);
|
|
container.scrollTop = container.scrollHeight;
|
|
eventCount++;
|
|
}
|
|
|
|
function clearEvents() {
|
|
const container = document.getElementById('events');
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🤔</div>
|
|
<p>Waiting for thinking events...</p>
|
|
<p style="font-size: 12px; margin-top: 10px;">Events will appear here when Lyra uses tools</p>
|
|
</div>
|
|
`;
|
|
eventCount = 0;
|
|
}
|
|
|
|
// Cleanup on page unload
|
|
window.addEventListener('beforeunload', () => {
|
|
if (eventSource) {
|
|
eventSource.close();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|