feat: Implement Trillium notes executor for searching and creating notes via ETAPI
- 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.
This commit is contained in:
362
core/ui/thinking-stream.html
Normal file
362
core/ui/thinking-stream.html
Normal file
@@ -0,0 +1,362 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user