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:
serversdwn
2025-12-26 03:49:20 -05:00
parent f1471cde84
commit 64429b19e6
37 changed files with 3238 additions and 23 deletions

View 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>