diff --git a/.gitea/workflows/datadog-sca.yml b/.gitea/workflows/datadog-sca.yml
new file mode 100644
index 0000000..2c99cb9
--- /dev/null
+++ b/.gitea/workflows/datadog-sca.yml
@@ -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
diff --git a/.gitea/workflows/datadog-static-analysis.yml b/.gitea/workflows/datadog-static-analysis.yml
new file mode 100644
index 0000000..f183ff3
--- /dev/null
+++ b/.gitea/workflows/datadog-static-analysis.yml
@@ -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
diff --git a/.github/workflows/datadog-sca.yml b/.github/workflows/datadog-sca.yml
new file mode 100644
index 0000000..2c99cb9
--- /dev/null
+++ b/.github/workflows/datadog-sca.yml
@@ -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
diff --git a/.github/workflows/datadog-static-analysis.yml b/.github/workflows/datadog-static-analysis.yml
new file mode 100644
index 0000000..f183ff3
--- /dev/null
+++ b/.github/workflows/datadog-static-analysis.yml
@@ -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
diff --git a/.gitignore b/.gitignore
index 80abc65..3dea82b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -174,3 +174,46 @@ cython_debug/
pyvenv.cfg
.venv
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
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..8a58831
--- /dev/null
+++ b/Dockerfile
@@ -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"]
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2887b5f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+# Jarvis
+
+it's actually not that smart!
+
+
diff --git a/client.py b/client.py
new file mode 100644
index 0000000..4613500
--- /dev/null
+++ b/client.py
@@ -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()
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..e11985b
--- /dev/null
+++ b/docker-compose.yml
@@ -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:
\ No newline at end of file
diff --git a/index.html b/index.html
index 654a579..8ebd44d 100644
--- a/index.html
+++ b/index.html
@@ -9,6 +9,8 @@
+
+
+
+
+
+
+
Conversation History
+
+
@@ -280,10 +394,80 @@
const chatContainer = document.getElementById('chat-container');
const userInput = document.getElementById('user-input');
const sendButton = document.getElementById('send-button');
+ const chatTabs = document.getElementById('chat-tabs');
- let thinkingElement = null;
- let thinkingDetails = null;
- let thinkingStartTime = null;
+ let currentChatId = null;
+ let chats = {};
+
+ 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) {
const messageElement = document.createElement('div');
@@ -292,65 +476,40 @@
messageElement.innerHTML = isUser ? message : marked.parse(message);
chatContainer.appendChild(messageElement);
chatContainer.scrollTop = chatContainer.scrollHeight;
+
+ if (currentChatId) {
+ chats[currentChatId].messages.push({ content: message, isUser: isUser });
+ saveChats();
+ }
}
- function startThinking() {
- thinkingElement = document.createElement('div');
- thinkingElement.classList.add('thought-summary', 'collapsible');
+ function addThought(type, content, details = '', thinkingSection) {
+ const stepElement = document.createElement('div');
+ 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');
- led.classList.add('led', 'blinking');
-
- const textNode = document.createTextNode('Thinking...');
-
- thinkingElement.appendChild(led);
- thinkingElement.appendChild(textNode);
- thinkingElement.onclick = toggleThinkingDetails;
-
- thinkingDetails = document.createElement('div');
- thinkingDetails.classList.add('thought-details');
-
- chatContainer.appendChild(thinkingElement);
- chatContainer.appendChild(thinkingDetails);
-
- thinkingStartTime = Date.now();
+ if (type === 'error') {
+ stepElement.classList.add('error-message');
+ if (content.includes('retrying')) {
+ stepElement.classList.add('retrying');
+ }
+ stepDetails.innerHTML = marked.parse(content + '\n\nDetails:\n```\n' + details + '\n```');
+ } else {
+ stepDetails.innerHTML = marked.parse(content);
+ }
+
+ thinkingSection.appendChild(stepElement);
+ thinkingSection.appendChild(stepDetails);
chatContainer.scrollTop = chatContainer.scrollHeight;
- }
- function addThought(step, content) {
- if (thinkingDetails) {
- const stepElement = document.createElement('div');
- stepElement.classList.add('thought-summary', 'collapsible');
- 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';
+ if (currentChatId) {
+ const currentThinkingSection = chats[currentChatId].thinkingSections[chats[currentChatId].thinkingSections.length - 1];
+ currentThinkingSection.thoughts.push({ type, content, details });
+ saveChats();
}
}
@@ -362,34 +521,71 @@
}
}
- socket.on('thinking', (data) => {
- if (!thinkingElement) startThinking();
- addThought(data.step, 'Started');
- });
+ function saveChats() {
+ localStorage.setItem('chats', JSON.stringify(chats));
+ }
- socket.on('thought', (data) => {
- addThought('Result', data.content);
- });
-
- socket.on('chat_response', (data) => {
- endThinking(data.thinking_time);
- addMessage(data.response, false);
- });
-
- socket.on('error', (data) => {
- endThinking(data.thinking_time);
- addMessage(`Error: ${data.message}`, false);
- });
+ function loadChats() {
+ const storedChats = localStorage.getItem('chats');
+ if (storedChats) {
+ chats = JSON.parse(storedChats);
+ Object.keys(chats).forEach(chatId => addChatTab(chatId));
+ if (Object.keys(chats).length > 0) {
+ switchToChat(Object.keys(chats)[0]);
+ } else {
+ createNewChat();
+ }
+ } else {
+ createNewChat();
+ }
+ }
function sendMessage() {
const message = userInput.value.trim();
- if (message) {
+ if (message && currentChatId) {
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 = '';
}
}
+ 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);
userInput.addEventListener('keypress', function(e) {
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 = {
type: 'line',
options: {
@@ -570,6 +776,41 @@
window.addEventListener('resize', checkWindowSize);
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();
+ }
+ });