21 Commits

Author SHA1 Message Date
eda6cdbdf0 another build trigger?
Some checks failed
Datadog Software Composition Analysis / Datadog SBOM Generation and Upload (push) Failing after 55s
Datadog Static Analysis / Datadog Static Analyzer (push) Successful in 1m37s
2024-10-24 11:48:05 -04:00
23fa40723a another build trigger? 2024-10-24 11:43:42 -04:00
b862358902 empty readme to trigger build 2024-10-24 11:40:50 -04:00
1833420460 give it a try 2024-10-24 11:37:36 -04:00
131bb6d60f add some github actions 2024-10-24 11:26:51 -04:00
1de9ba68ac Add docker 2024-10-07 17:24:35 -04:00
58d63c27ae sql gen 2024-10-06 23:54:25 -04:00
6d0134c34d sql gen 2024-10-06 23:51:01 -04:00
f56d9b59c0 sql gen 2024-10-06 23:19:57 -04:00
0c9059bcbc sql gen 2024-10-06 21:50:57 -04:00
434a6ea6fc sql gen 2024-10-06 21:43:49 -04:00
c8e2d46013 sql gen 2024-10-06 21:34:09 -04:00
bbc4bd9115 sql gen 2024-10-06 21:32:16 -04:00
5111ae6a7b reqs 2024-10-06 21:28:25 -04:00
a888e84079 Trash frontend, cleanup, add DB 2024-10-06 21:23:55 -04:00
d90d4fe340 fix empty thoughts as a reply 2024-10-02 19:48:25 -04:00
26514e9fdd switched to qwen model, better prompt, easier parsing 2024-10-02 19:33:14 -04:00
9d25dd7d94 New frontend and tools 2024-10-01 19:31:57 -04:00
b56619a2e6 Next frontend 2024-09-29 12:22:04 -04:00
90e0b4ff7f Add next frontend 2024-09-29 12:22:03 -04:00
47059dabdc whole bucn of nonsense 2024-09-29 12:18:44 -04:00
15 changed files with 1818 additions and 337 deletions

View File

@ -0,0 +1,20 @@
on: [push]
name: Datadog Software Composition Analysis
jobs:
software-composition-analysis:
runs-on: ubuntu-latest
name: Datadog SBOM Generation and Upload
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Check imported libraries are secure and compliant
id: datadog-software-composition-analysis
uses: DataDog/datadog-sca-github-action@main
with:
dd_api_key: ${{ secrets.DD_API_KEY }}
dd_app_key: ${{ secrets.DD_APP_KEY }}
dd_service: jarvis
dd_env: ci
dd_site: us5.datadoghq.com

View File

@ -0,0 +1,21 @@
on: [push]
name: Datadog Static Analysis
jobs:
static-analysis:
runs-on: ubuntu-latest
name: Datadog Static Analyzer
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Check code meets quality and security standards
id: datadog-static-analysis
uses: DataDog/datadog-static-analyzer-github-action@v1
with:
dd_api_key: ${{ secrets.DD_API_KEY }}
dd_app_key: ${{ secrets.DD_APP_KEY }}
dd_service: jarvis
dd_env: ci
dd_site: us5.datadoghq.com
cpu_count: 2

20
.github/workflows/datadog-sca.yml vendored Normal file
View File

@ -0,0 +1,20 @@
on: [push]
name: Datadog Software Composition Analysis
jobs:
software-composition-analysis:
runs-on: ubuntu-latest
name: Datadog SBOM Generation and Upload
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Check imported libraries are secure and compliant
id: datadog-software-composition-analysis
uses: DataDog/datadog-sca-github-action@main
with:
dd_api_key: ${{ secrets.DD_API_KEY }}
dd_app_key: ${{ secrets.DD_APP_KEY }}
dd_service: jarvis
dd_env: ci
dd_site: us5.datadoghq.com

View File

@ -0,0 +1,21 @@
on: [push]
name: Datadog Static Analysis
jobs:
static-analysis:
runs-on: ubuntu-latest
name: Datadog Static Analyzer
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Check code meets quality and security standards
id: datadog-static-analysis
uses: DataDog/datadog-static-analyzer-github-action@v1
with:
dd_api_key: ${{ secrets.DD_API_KEY }}
dd_app_key: ${{ secrets.DD_APP_KEY }}
dd_service: jarvis
dd_env: ci
dd_site: us5.datadoghq.com
cpu_count: 2

43
.gitignore vendored
View File

@ -174,3 +174,46 @@ cython_debug/
pyvenv.cfg pyvenv.cfg
.venv .venv
pip-selfcheck.json pip-selfcheck.json
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
.next
config.ini
*.db

20
Dockerfile Normal file
View File

@ -0,0 +1,20 @@
# Use an official Python runtime as a parent image
FROM python:3.9-slim
# Set the working directory in the container
WORKDIR /app
# Copy the current directory contents into the container at /app
COPY . /app
# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Make port 5001 available to the world outside this container
EXPOSE 5001
# Define environment variable
ENV FLASK_APP=main.py
# Run app.py when the container launches
CMD ["python", "main.py"]

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Jarvis
it's actually not that smart!

138
client.py Normal file
View File

@ -0,0 +1,138 @@
import time
import requests
class LLMChatClient:
def __init__(self, base_url, api_key):
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self.headers = {"X-API-Key": api_key, "Content-Type": "application/json"}
def submit_query(self, message):
"""
Submit a query to the LLM Chat Server.
Args:
message (str): The message to send to the server.
Returns:
str: The query ID for the submitted query.
Raises:
requests.RequestException: If the request fails.
Example:
client = LLMChatClient('http://localhost:5001', 'your-api-key')
query_id = client.submit_query('What is the capital of France?')
print(f"Query ID: {query_id}")
cURL equivalent:
curl -X POST http://localhost:5001/api/v1/query \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{"message": "What is the capital of France?"}'
"""
url = f"{self.base_url}/api/v1/query"
data = {"message": message}
response = requests.post(url, json=data, headers=self.headers)
response.raise_for_status()
return response.json()["query_id"]
def get_query_status(self, query_id):
"""
Get the status of a submitted query.
Args:
query_id (str): The ID of the query to check.
Returns:
dict: A dictionary containing the status and conversation history (if completed).
Raises:
requests.RequestException: If the request fails.
Example:
client = LLMChatClient('http://localhost:5001', 'your-api-key')
status = client.get_query_status('query-id-here')
print(f"Query status: {status['status']}")
if status['status'] == 'completed':
print(f"Conversation history: {status['conversation_history']}")
cURL equivalent:
curl -X GET http://localhost:5001/api/v1/query_status/query-id-here \
-H "X-API-Key: your-api-key"
"""
url = f"{self.base_url}/api/v1/query_status/{query_id}"
response = requests.get(url, headers=self.headers)
response.raise_for_status()
return response.json()
def submit_query_and_wait(self, message, max_wait_time=300, poll_interval=2):
"""
Submit a query and wait for the result.
Args:
message (str): The message to send to the server.
max_wait_time (int): Maximum time to wait for the result in seconds.
poll_interval (int): Time between status checks in seconds.
Returns:
dict: The completed conversation history.
Raises:
requests.RequestException: If the request fails.
TimeoutError: If the query doesn't complete within max_wait_time.
Example:
client = LLMChatClient('http://localhost:5001', 'your-api-key')
result = client.submit_query_and_wait('What is the capital of France?')
print(f"Conversation history: {result}")
"""
query_id = self.submit_query(message)
start_time = time.time()
while time.time() - start_time < max_wait_time:
status = self.get_query_status(query_id)
if status["status"] == "completed":
return status["conversation_history"]
time.sleep(poll_interval)
raise TimeoutError(f"Query did not complete within {max_wait_time} seconds")
class LLMChatAdminClient:
def __init__(self, base_url, admin_key):
self.base_url = base_url.rstrip("/")
self.admin_key = admin_key
self.headers = {"X-Admin-Key": admin_key, "Content-Type": "application/json"}
def generate_api_key(self, username):
"""
Generate a new API key for a user.
Args:
username (str): The username to generate the API key for.
Returns:
dict: A dictionary containing the username and generated API key.
Raises:
requests.RequestException: If the request fails.
Example:
admin_client = LLMChatAdminClient('http://localhost:5001', 'your-admin-key')
result = admin_client.generate_api_key('new_user')
print(f"Generated API key for {result['username']}: {result['api_key']}")
cURL equivalent:
curl -X POST http://localhost:5001/admin/generate_key \
-H "Content-Type: application/json" \
-H "X-Admin-Key: your-admin-key" \
-d '{"username": "new_user"}'
"""
url = f"{self.base_url}/admin/generate_key"
data = {"username": username}
response = requests.post(url, json=data, headers=self.headers)
response.raise_for_status()
return response.json()

16
docker-compose.yml Normal file
View File

@ -0,0 +1,16 @@
version: '3.8'
services:
llm-chat-server:
build: .
ports:
- "5001:5001"
volumes:
- ./llm_chat_server.db:/app/llm_chat_server.db
- ./config.ini:/app/config.ini
environment:
- FLASK_ENV=production
restart: unless-stopped
volumes:
llm_chat_server_db:

View File

@ -9,6 +9,8 @@
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@1.0.1/dist/chartjs-adapter-moment.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@1.0.1/dist/chartjs-adapter-moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Mono:wght@400;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Mono:wght@400;700&display=swap" rel="stylesheet">
<style> <style>
body { body {
@ -86,13 +88,19 @@
color: #888; color: #888;
} }
.thought-summary { .thought-summary {
cursor: pointer;
color: #fff;
margin-bottom: 5px;
font-weight: bold; font-weight: bold;
display: flex; margin-bottom: 5px;
align-items: center; padding: 5px;
border-radius: 3px;
} }
.thought-summary.plan { background-color: #2c3e50; }
.thought-summary.decision { background-color: #34495e; }
.thought-summary.tool_call { background-color: #16a085; }
.thought-summary.tool_result { background-color: #27ae60; }
.thought-summary.think_more { background-color: #2980b9; }
.thought-summary.answer { background-color: #8e44ad; }
.thought-summary.reply { background-color: #f39c12; }
.thought-summary.thoughts { background-color: #f39c12; }
.thought-details { .thought-details {
display: none; display: none;
margin-left: 20px; margin-left: 20px;
@ -238,11 +246,111 @@
transform: translateX(0); transform: translateX(0);
} }
} }
.conversation-history-container {
margin-top: 20px;
background-color: #222;
border-radius: 5px;
padding: 10px;
}
#conversation-history {
color: #fff;
font-family: 'Noto Sans Mono', monospace;
font-size: 12px;
}
.history-card {
background-color: #2c3e50;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
}
.history-role {
font-weight: bold;
margin-bottom: 5px;
}
.history-content {
white-space: pre-wrap;
word-break: break-word;
}
.error-message {
background-color: #ff6b6b;
color: #fff;
padding: 10px;
border-radius: 5px;
margin-bottom: 10px;
}
.retrying {
background-color: #feca57;
color: #333;
}
#clear-history-button {
background-color: #e74c3c;
color: white;
border: none;
padding: 10px;
margin-bottom: 10px;
cursor: pointer;
font-family: 'Noto Sans Mono', monospace;
font-size: 14px;
border-radius: 5px;
}
#clear-history-button:hover {
background-color: #c0392b;
}
#chat-tabs {
display: flex;
background-color: #222;
padding: 10px 10px 0 10px;
}
.chat-tab {
background-color: #444;
color: #fff;
border: none;
padding: 10px 20px;
margin-right: 5px;
cursor: pointer;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
.chat-tab.active {
background-color: #666;
}
#new-chat-button {
background-color: #27ae60;
color: #fff;
border: none;
padding: 10px 20px;
cursor: pointer;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
.close-tab {
margin-left: 10px;
color: #ff6b6b;
cursor: pointer;
}
.thinking-section {
margin-bottom: 20px;
border-left: 2px solid #444;
padding-left: 10px;
}
</style> </style>
</head> </head>
<body> <body>
<div id="main-container"> <div id="main-container">
<div id="chat-area"> <div id="chat-area">
<div id="chat-tabs"></div>
<div id="chat-container"></div> <div id="chat-container"></div>
<div id="input-container" class="pdp-panel"> <div id="input-container" class="pdp-panel">
<div class="pdp-label">INPUT:</div> <div class="pdp-label">INPUT:</div>
@ -272,6 +380,12 @@
<div class="graph-title">GPU Memory</div> <div class="graph-title">GPU Memory</div>
<canvas id="gpuMemoryChart"></canvas> <canvas id="gpuMemoryChart"></canvas>
</div> </div>
<!-- Add this new section for conversation history -->
<div class="conversation-history-container">
<div class="graph-title">Conversation History</div>
<div id="conversation-history"></div>
</div>
</div> </div>
</div> </div>
@ -280,10 +394,80 @@
const chatContainer = document.getElementById('chat-container'); const chatContainer = document.getElementById('chat-container');
const userInput = document.getElementById('user-input'); const userInput = document.getElementById('user-input');
const sendButton = document.getElementById('send-button'); const sendButton = document.getElementById('send-button');
const chatTabs = document.getElementById('chat-tabs');
let thinkingElement = null; let currentChatId = null;
let thinkingDetails = null; let chats = {};
let thinkingStartTime = null;
function createNewChat() {
const chatId = Date.now().toString();
chats[chatId] = {
messages: [],
thinkingSections: []
};
addChatTab(chatId);
switchToChat(chatId);
saveChats();
}
function addChatTab(chatId) {
const tab = document.createElement('button');
tab.classList.add('chat-tab');
tab.textContent = `Chat ${Object.keys(chats).length}`;
tab.onclick = () => switchToChat(chatId);
const closeButton = document.createElement('span');
closeButton.classList.add('close-tab');
closeButton.textContent = '×';
closeButton.onclick = (e) => {
e.stopPropagation();
closeChat(chatId);
};
tab.appendChild(closeButton);
chatTabs.insertBefore(tab, chatTabs.lastElementChild);
}
function switchToChat(chatId) {
currentChatId = chatId;
document.querySelectorAll('.chat-tab').forEach(tab => tab.classList.remove('active'));
document.querySelector(`.chat-tab:nth-child(${Object.keys(chats).indexOf(chatId) + 1})`).classList.add('active');
renderChat(chatId);
}
function closeChat(chatId) {
delete chats[chatId];
saveChats();
const tabToRemove = Array.from(chatTabs.children).find(tab => tab.textContent.includes(`Chat ${Object.keys(chats).indexOf(chatId) + 1}`));
if (tabToRemove) {
chatTabs.removeChild(tabToRemove);
}
if (currentChatId === chatId) {
const remainingChatIds = Object.keys(chats);
if (remainingChatIds.length > 0) {
switchToChat(remainingChatIds[0]);
} else {
createNewChat();
}
}
}
function renderChat(chatId) {
chatContainer.innerHTML = '';
const chat = chats[chatId];
chat.messages.forEach(message => addMessage(message.content, message.isUser));
chat.thinkingSections.forEach(section => {
const thinkingSection = createThinkingSection();
section.thoughts.forEach(thought => addThought(thought.type, thought.content, thought.details, thinkingSection));
});
}
function createThinkingSection() {
const section = document.createElement('div');
section.classList.add('thinking-section');
chatContainer.appendChild(section);
return section;
}
function addMessage(message, isUser) { function addMessage(message, isUser) {
const messageElement = document.createElement('div'); const messageElement = document.createElement('div');
@ -292,65 +476,40 @@
messageElement.innerHTML = isUser ? message : marked.parse(message); messageElement.innerHTML = isUser ? message : marked.parse(message);
chatContainer.appendChild(messageElement); chatContainer.appendChild(messageElement);
chatContainer.scrollTop = chatContainer.scrollHeight; chatContainer.scrollTop = chatContainer.scrollHeight;
if (currentChatId) {
chats[currentChatId].messages.push({ content: message, isUser: isUser });
saveChats();
}
} }
function startThinking() { function addThought(type, content, details = '', thinkingSection) {
thinkingElement = document.createElement('div'); const stepElement = document.createElement('div');
thinkingElement.classList.add('thought-summary', 'collapsible'); stepElement.classList.add('thought-summary', 'collapsible', type);
stepElement.textContent = type.charAt(0).toUpperCase() + type.slice(1).replace('_', ' ') + ':';
stepElement.onclick = toggleStepDetails;
const stepDetails = document.createElement('div');
stepDetails.classList.add('thought-details');
const led = document.createElement('div'); if (type === 'error') {
led.classList.add('led', 'blinking'); stepElement.classList.add('error-message');
if (content.includes('retrying')) {
const textNode = document.createTextNode('Thinking...'); stepElement.classList.add('retrying');
}
thinkingElement.appendChild(led); stepDetails.innerHTML = marked.parse(content + '\n\nDetails:\n```\n' + details + '\n```');
thinkingElement.appendChild(textNode); } else {
thinkingElement.onclick = toggleThinkingDetails; stepDetails.innerHTML = marked.parse(content);
}
thinkingDetails = document.createElement('div');
thinkingDetails.classList.add('thought-details'); thinkingSection.appendChild(stepElement);
thinkingSection.appendChild(stepDetails);
chatContainer.appendChild(thinkingElement);
chatContainer.appendChild(thinkingDetails);
thinkingStartTime = Date.now();
chatContainer.scrollTop = chatContainer.scrollHeight; chatContainer.scrollTop = chatContainer.scrollHeight;
}
function addThought(step, content) { if (currentChatId) {
if (thinkingDetails) { const currentThinkingSection = chats[currentChatId].thinkingSections[chats[currentChatId].thinkingSections.length - 1];
const stepElement = document.createElement('div'); currentThinkingSection.thoughts.push({ type, content, details });
stepElement.classList.add('thought-summary', 'collapsible'); saveChats();
stepElement.textContent = step;
stepElement.onclick = toggleStepDetails;
const stepDetails = document.createElement('div');
stepDetails.classList.add('thought-details');
stepDetails.innerHTML = content;
thinkingDetails.appendChild(stepElement);
thinkingDetails.appendChild(stepDetails);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
}
function endThinking(thinkingTime) {
if (thinkingElement) {
const textNode = thinkingElement.childNodes[1];
textNode.nodeValue = `Thinking... (${thinkingTime}s)`;
const led = thinkingElement.querySelector('.led');
led.classList.remove('blinking');
led.style.backgroundColor = '#0f0';
led.style.boxShadow = '0 0 10px #0f0';
thinkingStartTime = null;
}
}
function toggleThinkingDetails() {
this.classList.toggle('open');
const details = this.nextElementSibling;
if (details) {
details.style.display = details.style.display === 'none' ? 'block' : 'none';
} }
} }
@ -362,34 +521,71 @@
} }
} }
socket.on('thinking', (data) => { function saveChats() {
if (!thinkingElement) startThinking(); localStorage.setItem('chats', JSON.stringify(chats));
addThought(data.step, 'Started'); }
});
socket.on('thought', (data) => { function loadChats() {
addThought('Result', data.content); const storedChats = localStorage.getItem('chats');
}); if (storedChats) {
chats = JSON.parse(storedChats);
socket.on('chat_response', (data) => { Object.keys(chats).forEach(chatId => addChatTab(chatId));
endThinking(data.thinking_time); if (Object.keys(chats).length > 0) {
addMessage(data.response, false); switchToChat(Object.keys(chats)[0]);
}); } else {
createNewChat();
socket.on('error', (data) => { }
endThinking(data.thinking_time); } else {
addMessage(`Error: ${data.message}`, false); createNewChat();
}); }
}
function sendMessage() { function sendMessage() {
const message = userInput.value.trim(); const message = userInput.value.trim();
if (message) { if (message && currentChatId) {
addMessage(message, true); addMessage(message, true);
socket.emit('chat_request', { message: message }); chats[currentChatId].thinkingSections.push({ thoughts: [] });
socket.emit('chat_request', {
message: message,
conversation_history: chats[currentChatId].messages.filter(m => !m.isUser).map(m => ({ role: 'assistant', content: m.content }))
.concat(chats[currentChatId].messages.filter(m => m.isUser).map(m => ({ role: 'user', content: m.content })))
});
userInput.value = ''; userInput.value = '';
} }
} }
socket.on('thinking', (data) => {
if (currentChatId) {
const newThinkingSection = createThinkingSection();
chats[currentChatId].thinkingSections.push({ thoughts: [] });
addThought(data.step, 'Started', '', newThinkingSection);
}
});
socket.on('thought', (data) => {
if (currentChatId) {
const currentThinkingSection = chatContainer.querySelector('.thinking-section:last-child');
addThought(data.type, data.content, data.details, currentThinkingSection);
}
});
socket.on('chat_response', (data) => {
if (currentChatId) {
addMessage(data.response, false);
}
});
socket.on('error', (data) => {
if (currentChatId) {
const currentThinkingSection = chatContainer.querySelector('.thinking-section:last-child');
if (data.type === 'retrying') {
addThought('error', data.content, '', currentThinkingSection);
} else {
addThought('error', data.message, '', currentThinkingSection);
}
}
});
sendButton.addEventListener('click', sendMessage); sendButton.addEventListener('click', sendMessage);
userInput.addEventListener('keypress', function(e) { userInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
@ -398,6 +594,16 @@
} }
}); });
// Add new chat button
const newChatButton = document.createElement('button');
newChatButton.id = 'new-chat-button';
newChatButton.textContent = '+ New Chat';
newChatButton.onclick = createNewChat;
chatTabs.appendChild(newChatButton);
// Load chats when the page loads
loadChats();
const chartOptions = { const chartOptions = {
type: 'line', type: 'line',
options: { options: {
@ -570,6 +776,41 @@
window.addEventListener('resize', checkWindowSize); window.addEventListener('resize', checkWindowSize);
checkWindowSize(); // Initial check checkWindowSize(); // Initial check
// Add this new function to update the conversation history
function updateConversationHistory(history) {
const conversationHistoryElement = document.getElementById('conversation-history');
conversationHistoryElement.innerHTML = '';
history.forEach(item => {
const card = document.createElement('div');
card.classList.add('history-card');
const role = document.createElement('div');
role.classList.add('history-role');
role.textContent = item.role.charAt(0).toUpperCase() + item.role.slice(1);
const content = document.createElement('pre');
content.classList.add('history-content');
content.innerHTML = hljs.highlightAuto(item.content).value;
card.appendChild(role);
card.appendChild(content);
conversationHistoryElement.appendChild(card);
});
}
// Add this new socket listener
socket.on('conversation_history', (data) => {
updateConversationHistory(data.history);
});
// Add event listener for the clear history button
clearHistoryButton.addEventListener('click', () => {
if (confirm('Are you sure you want to clear the conversation history?')) {
clearConversationHistory();
}
});
</script> </script>
</body> </body>
</html> </html>

981
main.py

File diff suppressed because it is too large Load Diff

View File

@ -3,30 +3,86 @@ import structlog
logger = structlog.get_logger() logger = structlog.get_logger()
class ModelManager: class ModelManager:
def __init__(self): def __init__(self):
self.model_capabilities = { self.model_capabilities = {
"qwen2.5:7b": ["general_knowledge", "structured_output", "multilingual", "instruction_following", "structured_data"], "ajindal/llama3.1-storm:8b": [
"llama3.1:8b": ["general_knowledge", "reasoning", "tool_calling", "conversation", "multilingual", "instruction_following"], "general_knowledge",
"qwen2.5-coder:7b": ["code_generation", "code_analysis", "instruction_following", "math_reasoning"], "reasoning",
"llama3.2:3b": ["summarization", "instruction_following", "tool_calling", "multilingual"], "tool_calling",
"llava:7b": ["visual_reasoning", "visual_conversation", "visual_tool_calling", "vision", "ocr", "multimodal"], "conversation",
"multilingual",
"instruction_following",
],
"llama3.1:8b": [
"general_knowledge",
"reasoning",
"tool_calling",
"conversation",
"multilingual",
"instruction_following",
],
"qwen2.5:7b": [
"general_knowledge",
"reasoning",
"tool_calling",
"conversation",
"multilingual",
"instruction_following",
],
"llama3.2:3b": [
"summarization",
"instruction_following",
"tool_calling",
"multilingual",
],
"llava:7b": [
"visual_reasoning",
"visual_conversation",
"visual_tool_calling",
"vision",
"ocr",
"multimodal",
],
} }
logger.info("ModelManager initialized", model_capabilities=self.model_capabilities) logger.info(
"ModelManager initialized", model_capabilities=self.model_capabilities
)
def get_model_capabilities(self, model_name): def get_model_capabilities(self, model_name):
capabilities = self.model_capabilities.get(model_name, []) capabilities = self.model_capabilities.get(model_name, [])
logger.debug("Retrieved model capabilities", model=model_name, capabilities=capabilities) logger.debug(
"Retrieved model capabilities", model=model_name, capabilities=capabilities
)
return capabilities return capabilities
def select_best_model(self, required_capability): def select_best_model(self, required_capability):
suitable_models = [model for model, capabilities in self.model_capabilities.items() if required_capability in capabilities] suitable_models = [
selected_model = suitable_models[0] if suitable_models else list(self.model_capabilities.keys())[0] model
logger.info("Selected best model", required_capability=required_capability, selected_model=selected_model) for model, capabilities in self.model_capabilities.items()
if required_capability in capabilities
]
selected_model = (
suitable_models[0]
if suitable_models
else list(self.model_capabilities.keys())[0]
)
logger.info(
"Selected best model",
required_capability=required_capability,
selected_model=selected_model,
)
return selected_model return selected_model
def generate_text(self, model_name, prompt, max_length=100, system="You are a helpful assistant.", stream=False): def generate_text(
logger.debug("Generating text", model=model_name, prompt=prompt, max_length=max_length) self,
model_name,
prompt,
max_length=100,
system="You are a helpful assistant.",
tools=[],
):
# Check if model exists # Check if model exists
try: try:
ollama.pull(model_name) ollama.pull(model_name)
@ -38,8 +94,16 @@ class ModelManager:
else: else:
logger.exception("Error pulling model", model=model_name, error=str(e)) logger.exception("Error pulling model", model=model_name, error=str(e))
raise e raise e
response = ollama.generate(model=model_name, prompt=prompt, system=system, stream=stream)
logger.debug("Text generated", model=model_name, response=response['response'])
return response['response']
model_manager = ModelManager() response = ollama.generate(
model=model_name,
prompt=prompt,
system=system,
tools=tools,
max_tokens=max_length,
)
logger.debug("Text generated", model=model_name, response=response["response"])
return response["response"]
model_manager = ModelManager()

View File

@ -4,11 +4,19 @@ aiohttp==3.10.5
aiosignal==1.3.1 aiosignal==1.3.1
annotated-types==0.7.0 annotated-types==0.7.0
anyio==4.6.0 anyio==4.6.0
art==6.3
attrs==24.2.0 attrs==24.2.0
beautifulsoup4==4.12.3
bidict==0.23.1
black==24.8.0
blinker==1.8.2
bs4==0.0.2
certifi==2024.7.4 certifi==2024.7.4
chardet==5.2.0
charset-normalizer==3.3.2 charset-normalizer==3.3.2
click==8.1.7 click==8.1.7
cloudpickle==3.0.0 cloudpickle==3.0.0
cssselect==1.2.0
datasets==3.0.0 datasets==3.0.0
dill==0.3.8 dill==0.3.8
diskcache==5.6.3 diskcache==5.6.3
@ -17,9 +25,13 @@ duckduckgo_search==6.2.6
einops==0.8.0 einops==0.8.0
fastapi==0.115.0 fastapi==0.115.0
filelock==3.15.4 filelock==3.15.4
Flask==3.0.3
flask-openapi3==3.1.3
Flask-SocketIO==5.3.7
frozenlist==1.4.1 frozenlist==1.4.1
fsspec==2024.6.1 fsspec==2024.6.1
gguf==0.9.1 gguf==0.9.1
GPUtil==1.4.0
h11==0.14.0 h11==0.14.0
httpcore==1.0.5 httpcore==1.0.5
httptools==0.6.1 httptools==0.6.1
@ -29,6 +41,8 @@ idna==3.7
importlib_metadata==8.5.0 importlib_metadata==8.5.0
inquirerpy==0.3.4 inquirerpy==0.3.4
interegular==0.3.3 interegular==0.3.3
isort==5.13.2
itsdangerous==2.2.0
Jinja2==3.1.4 Jinja2==3.1.4
jiter==0.5.0 jiter==0.5.0
jsonschema==4.23.0 jsonschema==4.23.0
@ -36,6 +50,9 @@ jsonschema-specifications==2023.12.1
lark==1.2.2 lark==1.2.2
llvmlite==0.43.0 llvmlite==0.43.0
lm-format-enforcer==0.10.6 lm-format-enforcer==0.10.6
lxml==5.3.0
lxml_html_clean==0.2.2
markdownify==0.13.1
MarkupSafe==2.1.5 MarkupSafe==2.1.5
mistral_common==1.4.3 mistral_common==1.4.3
mpmath==1.3.0 mpmath==1.3.0
@ -43,6 +60,7 @@ msgpack==1.1.0
msgspec==0.18.6 msgspec==0.18.6
multidict==6.1.0 multidict==6.1.0
multiprocess==0.70.16 multiprocess==0.70.16
mypy-extensions==1.0.0
nest-asyncio==1.6.0 nest-asyncio==1.6.0
networkx==3.3 networkx==3.3
numba==0.60.0 numba==0.60.0
@ -60,13 +78,16 @@ nvidia-ml-py==12.560.30
nvidia-nccl-cu12==2.20.5 nvidia-nccl-cu12==2.20.5
nvidia-nvjitlink-cu12==12.6.20 nvidia-nvjitlink-cu12==12.6.20
nvidia-nvtx-cu12==12.1.105 nvidia-nvtx-cu12==12.1.105
ollama==0.3.3
openai==1.47.1 openai==1.47.1
outlines==0.0.46 outlines==0.0.46
packaging==24.1 packaging==24.1
pandas==2.2.3 pandas==2.2.3
partial-json-parser==0.2.1.1.post4 partial-json-parser==0.2.1.1.post4
pathspec==0.12.1
pfzy==0.3.4 pfzy==0.3.4
pillow==10.4.0 pillow==10.4.0
platformdirs==4.3.6
primp==0.5.5 primp==0.5.5
prometheus-fastapi-instrumentator==7.0.0 prometheus-fastapi-instrumentator==7.0.0
prometheus_client==0.21.0 prometheus_client==0.21.0
@ -81,10 +102,13 @@ pydantic==2.9.2
pydantic_core==2.23.4 pydantic_core==2.23.4
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-dotenv==1.0.1 python-dotenv==1.0.1
python-engineio==4.9.1
python-socketio==5.11.4
pytz==2024.2 pytz==2024.2
PyYAML==6.0.2 PyYAML==6.0.2
pyzmq==26.2.0 pyzmq==26.2.0
ray==2.36.1 ray==2.36.1
readability-lxml==0.8.1
referencing==0.35.1 referencing==0.35.1
regex==2024.7.24 regex==2024.7.24
requests==2.32.3 requests==2.32.3
@ -92,9 +116,12 @@ rpds-py==0.20.0
safetensors==0.4.4 safetensors==0.4.4
sentencepiece==0.2.0 sentencepiece==0.2.0
setuptools==72.1.0 setuptools==72.1.0
simple-websocket==1.0.0
six==1.16.0 six==1.16.0
sniffio==1.3.1 sniffio==1.3.1
soupsieve==2.6
starlette==0.38.6 starlette==0.38.6
structlog==24.4.0
sympy==1.13.2 sympy==1.13.2
tiktoken==0.7.0 tiktoken==0.7.0
tokenizers==0.19.1 tokenizers==0.19.1
@ -113,6 +140,8 @@ vllm-flash-attn==2.6.1
watchfiles==0.24.0 watchfiles==0.24.0
wcwidth==0.2.13 wcwidth==0.2.13
websockets==13.1 websockets==13.1
Werkzeug==3.0.4
wsproto==1.2.0
xformers==0.0.27.post2 xformers==0.0.27.post2
xxhash==3.5.0 xxhash==3.5.0
yarl==1.12.0 yarl==1.12.0

16
schema.sql Normal file
View File

@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS Keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
api_key TEXT NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS Queries (
id TEXT PRIMARY KEY,
ip TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
query TEXT NOT NULL,
api_key_id INTEGER,
status TEXT NOT NULL,
conversation_history TEXT,
FOREIGN KEY (api_key_id) REFERENCES Keys (id)
);

326
tools.py
View File

@ -1,16 +1,29 @@
import duckduckgo_search import subprocess
import tempfile
import time
import json
import requests import requests
from readability.readability import Document
from markdownify import markdownify as md from markdownify import markdownify as md
from readability.readability import Document
import duckduckgo_search
import datetime
import random
import math
import re
import base64
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
import ollama
import os
class Tool: class Tool:
def __init__(self, name: str, description: str, arguments: str, returns: str): def __init__(self, name: str, description: str, arguments: dict, returns: str):
self.name = name self.name = name
self.description = description self.description = description
self.arguments = arguments self.arguments = arguments
self.returns = returns self.returns = returns
def execute(self, arguments: str) -> str: def execute(self, arguments: dict) -> str:
pass pass
@ -26,10 +39,23 @@ class ToolManager:
if tool.name == name: if tool.name == name:
return tool return tool
return None return None
def get_tools_and_descriptions_for_prompt(self): def get_tools_and_descriptions_for_prompt(self):
return "\n".join([f"{tool.name}: {tool.description}" for tool in self.tools]) return "\n".join([f"{tool.name}: {tool.description}" for tool in self.tools])
def get_tools_for_ollama_dict(self):
return [
{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.arguments,
},
}
for tool in self.tools
]
class DefaultToolManager(ToolManager): class DefaultToolManager(ToolManager):
def __init__(self): def __init__(self):
@ -38,16 +64,35 @@ class DefaultToolManager(ToolManager):
self.add_tool(GetReadablePageContentsTool()) self.add_tool(GetReadablePageContentsTool())
self.add_tool(CalculatorTool()) self.add_tool(CalculatorTool())
self.add_tool(PythonCodeTool()) self.add_tool(PythonCodeTool())
self.add_tool(DateTimeTool())
self.add_tool(RandomNumberTool())
self.add_tool(RegexTool())
self.add_tool(Base64Tool())
self.add_tool(SimpleChartTool())
self.add_tool(LLAVAImageAnalysisTool())
class SearchTool(Tool): class SearchTool(Tool):
def __init__(self): def __init__(self):
super().__init__("search_web", "Search the internet for information -- Takes a search query as an argument", "query:string", "results:list[string]") super().__init__(
"search_web",
"Search the internet for information",
{
"type": "object",
"properties": {
"query": {"type": "string", "description": "The search query"}
},
},
"results:list[string]",
)
def execute(self, arg: dict) -> str:
try:
res = duckduckgo_search.DDGS().text(arg["query"], max_results=5)
return "\n\n".join([f"{r['title']}\n{r['body']}\n{r['href']}" for r in res])
except Exception as e:
return f"Error searching the web: {str(e)}"
def execute(self, arg: str) -> str:
res = duckduckgo_search.DDGS().text(arg, max_results=5)
return [f"{r['title']}\n{r['body']}\n{r['href']}" for r in res]
def get_readable_page_contents(url: str) -> str: def get_readable_page_contents(url: str) -> str:
try: try:
@ -58,28 +103,267 @@ def get_readable_page_contents(url: str) -> str:
return md(content) return md(content)
except Exception as e: except Exception as e:
return f"Error fetching readable content: {str(e)}" return f"Error fetching readable content: {str(e)}"
class GetReadablePageContentsTool(Tool): class GetReadablePageContentsTool(Tool):
def __init__(self): def __init__(self):
super().__init__("get_readable_page_contents", "Get the contents of a web page in a readable format -- Takes a url as an argument", "url:string", "contents:string") super().__init__(
"get_readable_page_contents",
"Get the contents of a web page in a readable format",
{
"type": "object",
"properties": {
"url": {"type": "string", "description": "The url of the web page"}
},
},
"contents:string",
)
def execute(self, arg: str) -> str: def execute(self, arg: dict) -> str:
return get_readable_page_contents(arg[0]) return get_readable_page_contents(arg["url"])
class CalculatorTool(Tool): class CalculatorTool(Tool):
def __init__(self): def __init__(self):
super().__init__("calculator", "Perform a calculation -- Takes a python mathematical expression as an argument", "expression:string", "result:string") super().__init__(
"calculator",
"Perform a calculation using python's eval function",
{
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "The mathematical expression to evaluate, should be a python mathematical expression",
}
},
},
"result:string",
)
def execute(self, arg: str) -> str: def execute(self, arg: dict) -> str:
return str(eval(arg[0])) try:
return str(eval(arg["expression"]))
except Exception as e:
return f"Error executing code: {str(e)}"
class PythonCodeTool(Tool): class PythonCodeTool(Tool):
def __init__(self): def __init__(self):
super().__init__("python_code", "Execute python code -- Takes a python code as an argument, code must be a single line of valid python", "code:string", "result:string") super().__init__(
"python_code",
"Execute python code using a temporary file and a subprocess. You must print results to stdout.",
{
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "The python code to execute, can be multiline",
}
},
},
"result:string",
)
def execute(self, arg: str) -> str: def execute(self, arg: dict) -> str:
return str(eval(arg[0])) try:
with tempfile.NamedTemporaryFile(
suffix=".py", mode="w", delete=False
) as temp_file:
temp_file.write(arg["code"])
temp_file.flush()
start_time = time.time()
process = subprocess.Popen(
["python", temp_file.name],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
stdout, stderr = process.communicate(timeout=10) # 10 second timeout
end_time = time.time()
execution_time = end_time - start_time
result = {
"stdout": stdout,
"stderr": stderr,
"return_value": process.returncode,
"execution_time": execution_time,
}
except subprocess.TimeoutExpired:
process.kill()
return "Error: Code execution timed out after 10 seconds"
except Exception as e:
return f"Error executing code: {str(e)}"
return "\n".join([f"{k}:\n{v}" for k, v in result.items()])
class DateTimeTool(Tool):
def __init__(self):
super().__init__(
"get_current_datetime",
"Get the current date and time",
{"type": "object", "properties": {}},
"datetime:string"
)
def execute(self, arg: dict) -> str:
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
class RandomNumberTool(Tool):
def __init__(self):
super().__init__(
"generate_random_number",
"Generate a random number within a given range",
{
"type": "object",
"properties": {
"min": {"type": "number", "description": "The minimum value"},
"max": {"type": "number", "description": "The maximum value"}
}
},
"random_number:number"
)
def execute(self, arg: dict) -> str:
return str(random.uniform(arg["min"], arg["max"]))
class RegexTool(Tool):
def __init__(self):
super().__init__(
"regex_match",
"Perform a regex match on a given text",
{
"type": "object",
"properties": {
"text": {"type": "string", "description": "The text to search in"},
"pattern": {"type": "string", "description": "The regex pattern to match"}
}
},
"matches:list[string]"
)
def execute(self, arg: dict) -> str:
matches = re.findall(arg["pattern"], arg["text"])
return json.dumps(matches)
class Base64Tool(Tool):
def __init__(self):
super().__init__(
"base64_encode_decode",
"Encode or decode a string using Base64",
{
"type": "object",
"properties": {
"action": {"type": "string", "enum": ["encode", "decode"], "description": "Whether to encode or decode"},
"text": {"type": "string", "description": "The text to encode or decode"}
}
},
"result:string"
)
def execute(self, arg: dict) -> str:
if arg["action"] == "encode":
return base64.b64encode(arg["text"].encode()).decode()
elif arg["action"] == "decode":
return base64.b64decode(arg["text"].encode()).decode()
else:
return "Invalid action. Use 'encode' or 'decode'."
class SimpleChartTool(Tool):
def __init__(self):
super().__init__(
"generate_simple_chart",
"Generate a simple bar chart image",
{
"type": "object",
"properties": {
"data": {"type": "array", "items": {"type": "number"}, "description": "List of numerical values for the chart"},
"labels": {"type": "array", "items": {"type": "string"}, "description": "Labels for each bar"}
}
},
"image_base64:string"
)
def execute(self, arg: dict) -> str:
data = arg["data"]
labels = arg["labels"]
# Create a simple bar chart
width, height = 400, 300
img = Image.new('RGB', (width, height), color='white')
draw = ImageDraw.Draw(img)
# Draw bars
max_value = max(data)
bar_width = width // (len(data) + 1)
for i, value in enumerate(data):
bar_height = (value / max_value) * (height - 50)
left = (i + 1) * bar_width
draw.rectangle([left, height - bar_height, left + bar_width, height], fill='blue')
# Add labels
font = ImageFont.load_default()
for i, label in enumerate(labels):
left = (i + 1) * bar_width + bar_width // 2
draw.text((left, height - 20), label, fill='black', anchor='ms', font=font)
# Convert to base64
buffered = BytesIO()
img.save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue()).decode()
return img_str
class LLAVAImageAnalysisTool(Tool):
def __init__(self):
super().__init__(
"analyze_image",
"Analyze an image using the LLAVA model",
{
"type": "object",
"properties": {
"image_base64": {"type": "string", "description": "Base64 encoded image"},
"question": {"type": "string", "description": "Question about the image"}
}
},
"analysis:string"
)
def execute(self, arg: dict) -> str:
try:
# Decode base64 image
image_data = base64.b64decode(arg["image_base64"])
image = Image.open(BytesIO(image_data))
# Save image to a temporary file
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file:
image.save(temp_file, format="PNG")
temp_file_path = temp_file.name
# Call LLAVA model
response = ollama.chat(
model="llava:7b",
messages=[
{
"role": "user",
"content": arg["question"],
"images": [temp_file_path]
}
]
)
# Clean up temporary file
os.remove(temp_file_path)
# Unload LLAVA model
ollama.delete("llava:7b")
return response['message']['content']
except Exception as e:
return f"Error analyzing image: {str(e)}"