Skip to content

Commit

Permalink
Merge pull request #16 from jhakulin/jhakulin/v2-migration-file-search
Browse files Browse the repository at this point in the history
Jhakulin/v2 migration file search
  • Loading branch information
jhakulin authored Apr 30, 2024
2 parents 569d716 + bf15304 commit 0af2933
Show file tree
Hide file tree
Showing 27 changed files with 2,104 additions and 742 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
💬 Inbuilt thread and memory management <br>
📊 Advanced Data Analysis, create data visualizations and solving complex code and math problems with **Code Interpreter**<br>
🚀 Build your own tools or call external tools and APIs with **Function Calling**<br>
📚 Retrieval Augmented Generation with **Retrieval** tool (coming soon to Azure OpenAI Assistants)<br>
📚 Retrieval Augmented Generation with **File Search** tool (coming soon to Azure OpenAI Assistants)<br>
🎤📢 Speech transcription and synthesis using Azure CognitiveServices Speech SDK<br>
📤 Exporting the assistant configuration into simple CLI application

Expand Down Expand Up @@ -92,7 +92,7 @@ Build the wheel for `azure.ai.assistant` library using the following instruction
- Go to the`sdk/azure-ai-assistant` folder
- Build the wheel using following command: `python setup.py sdist bdist_wheel`
- Go to generated `dist` folder
- Install the generated wheel using following command: `pip install --force-reinstall azure_ai_assistant-0.2.12a1-py3-none-any.whl`
- Install the generated wheel using following command: `pip install --force-reinstall azure_ai_assistant-0.3.0a1-py3-none-any.whl`
- This installation will pick the necessary dependencies for the library (openai, python-Levenshtein, fuzzywuzzy, Pillow, requests)

### Step 4: Install Python UI libraries
Expand Down
586 changes: 368 additions & 218 deletions gui/assistant_dialogs.py

Large diffs are not rendered by default.

43 changes: 31 additions & 12 deletions gui/conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,23 +318,42 @@ def replace_with_link(match):
return url_regex.sub(replace_with_link, text)

def format_file_links(self, text):
markdown_link_pattern = r'\[([^\]]+)\]\(sandbox:/mnt/data/([^)]+)\)'

# Pattern to find citations in the form [Download text]( [index])
citation_link_pattern = r'\[([^\]]+)\]\(\s*\[(\d+)\]\s*\)'
# Dictionary to store file paths indexed by the citation index
citation_to_filename = {}

# First, extract all file citations like "[0] finance_sector_revenue_chart.png"
file_citations = re.findall(r'\[(\d+)\]\s*(.+)', text)
for index, filename in file_citations:
citation_to_filename[index] = filename

# Function to replace citation links with clickable HTML links
def replace_with_clickable_text(match):
link_text = match.group(1)
file_name = match.group(2)
local_file_path = os.path.normpath(os.path.join(self.file_path, file_name))
citation_index = match.group(2)
file_name = citation_to_filename.get(citation_index)

if file_name:
local_file_path = os.path.normpath(os.path.join(self.file_path, file_name))

if link_text in self.text_to_url_map:
link_text = f"{link_text} {len(self.text_to_url_map) + 1}"

# Store the file path
self.text_to_url_map[link_text] = {"path": local_file_path}

# Return the HTML link and the local file path in separate inline-block divs
return (f'<div style="display: inline-block;"><a href="{local_file_path}" style="color:green; text-decoration: underline;" download="{file_name}">{link_text}</a></div>'
f'<div style="display: inline-block; color:gray;">{local_file_path}</div>')

if link_text in self.text_to_url_map:
link_text = f"{link_text} {len(self.text_to_url_map) + 1}"
# Store the file path
self.text_to_url_map[link_text] = {"path": local_file_path}
# Replace links in the original text
updated_text = re.sub(citation_link_pattern, replace_with_clickable_text, text)

# Return the HTML link and the local file path in separate inline-block divs
return (f'<div style="display: inline-block;"><a href="#" style="color:green; text-decoration: underline;" onclick="return false;">{link_text}</a></div>'
f'<div style="display: inline-block; color:gray;">{local_file_path}</div>')
# Remove the original citation lines
updated_text = re.sub(r'\[\d+\]\s*[^ ]+\.png', '', updated_text)

return re.sub(markdown_link_pattern, replace_with_clickable_text, text)
return updated_text

def parse_message(self, message):
"""Parse the message into a list of (is_code, text) tuples."""
Expand Down
148 changes: 86 additions & 62 deletions gui/conversation_sidebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,15 @@ def __init__(self, parent=None):
super().__init__(parent)
self.setAcceptDrops(True)
self.itemToFileMap = {} # Maps list items to attached file paths
self.itemToInstructionsMap = {} # Maps list items to additional instructions

def clear_files_and_instructions(self):
def clear_files(self):
self.itemToFileMap.clear()
self.itemToInstructionsMap.clear()

def contextMenuEvent(self, event):
context_menu = QMenu(self)
attach_file_action = context_menu.addAction("Attach File")
add_instructions_action = context_menu.addAction("Additional Instructions")

attach_file_search_action = context_menu.addAction("Attach File for File Search")
attach_file_code_action = context_menu.addAction("Attach File for Code Interpreter")
current_item = self.currentItem()
remove_file_menu = None
if current_item:
Expand All @@ -64,39 +62,24 @@ def contextMenuEvent(self, event):
remove_file_menu = context_menu.addMenu("Remove File")
for file_info in self.itemToFileMap[row]:
actual_file_path = file_info['file_path']
action = remove_file_menu.addAction(os.path.basename(actual_file_path))
tool_type = file_info['tools'][0]['type']

file_label = f"{os.path.basename(actual_file_path)} ({tool_type})"
action = remove_file_menu.addAction(file_label)
action.setData(file_info)

selected_action = context_menu.exec_(self.mapToGlobal(event.pos()))

if selected_action == attach_file_action:
self.attach_file_to_selected_item()
elif selected_action == add_instructions_action:
self.add_edit_instructions()
if selected_action == attach_file_search_action:
self.attach_file_to_selected_item("file_search")
elif selected_action == attach_file_code_action:
self.attach_file_to_selected_item("code_interpreter")
elif remove_file_menu and isinstance(selected_action, QAction) and selected_action.parent() == remove_file_menu:
file_info = selected_action.data()
self.remove_specific_file_from_selected_item(file_info, row)

def add_edit_instructions(self):
current_item = self.currentItem()
if current_item:
row = self.row(current_item)
current_instructions = self.itemToInstructionsMap.get(row, "")
text, ok = QInputDialog.getText(self, "Additional Instructions",
"Enter instructions:", text=current_instructions)
if ok:
self.itemToInstructionsMap[row] = text
attached_files = self.itemToFileMap.get(row, [])
self.update_item_icon(current_item, attached_files, text)

def get_instructions_for_selected_item(self):
current_item = self.currentItem()
if current_item:
row = self.row(current_item)
return self.itemToInstructionsMap.get(row, "")
return ""

def attach_file_to_selected_item(self):
def attach_file_to_selected_item(self, mode):
"""Attaches a file to the selected item with a specified mode indicating its intended use."""
file_dialog = QFileDialog(self)
file_path, _ = file_dialog.getOpenFileName(self, "Select File")
if file_path:
Expand All @@ -106,65 +89,90 @@ def attach_file_to_selected_item(self):
if row not in self.itemToFileMap:
self.itemToFileMap[row] = []

temp_file_id = "temp_" + os.path.basename(file_path)
self.itemToFileMap[row].append({"file_id": temp_file_id, "file_path": file_path})
self.itemToFileMap[row].append({
"file_id": None, # This will be updated later
"file_path": file_path,
"tools": [{"type": mode}] # Store the tool type for later use
})

current_instructions = self.itemToInstructionsMap.get(row, "")
self.update_item_icon(current_item, self.itemToFileMap[row], current_instructions)
self.update_item_icon(current_item, self.itemToFileMap[row])

def remove_specific_file_from_selected_item(self, file_info, row):
"""Removes a specific file from the selected item based on the file info provided."""
if row in self.itemToFileMap:
file_path_to_remove = file_info['file_path']
self.itemToFileMap[row] = [fi for fi in self.itemToFileMap[row] if fi['file_path'] != file_path_to_remove]

current_item = self.item(row)
current_instructions = self.itemToInstructionsMap.get(row, "")
if not self.itemToFileMap[row] and not current_instructions:
if not self.itemToFileMap[row]:
current_item.setIcon(QIcon())
else:
self.update_item_icon(current_item, self.itemToFileMap[row], current_instructions)
self.update_item_icon(current_item, self.itemToFileMap[row])

def update_item_icon(self, item, files, instructions):
if files or instructions:
def update_item_icon(self, item, files):
"""Updates the list item's icon based on whether there are attached files."""
if files:
item.setIcon(QIcon("gui/images/paperclip_icon.png"))
else:
item.setIcon(QIcon())

def get_attached_files_for_selected_item(self):
"""Return the file paths of files attached to the currently selected item."""
def get_attachments_for_selected_item(self):
"""Return the details of files attached to the currently selected item including file path and specific tool usage."""
current_item = self.currentItem()
if current_item:
row = self.row(current_item)
attached_files_info = self.itemToFileMap.get(row, [])
# Extract and return only the file paths
return [file_info['file_path'] for file_info in attached_files_info]
attachments = []
for file_info in attached_files_info:
file_path = file_info['file_path']
file_name = os.path.basename(file_path)
file_id = file_info.get('file_id', None)
tools = file_info.get('tools', [])

# Create a structured entry for the attachments list including file_path
attachments.append({
"file_name": file_name,
"file_id": file_id,
"file_path": file_path, # Include the full file path for upload or further processing
"tools": tools
})
return attachments
return []

def set_attachments_for_selected_item(self, attachments):
"""Set the attachments for the currently selected item."""
current_item = self.currentItem()
if current_item is not None:
row = self.row(current_item)
self.itemToFileMap[row] = attachments[:]
self.update_item_icon(current_item, attachments)
else:
logger.warning("No item is currently selected.")

def load_threads_with_files_and_instructions(self, threads):
"""Load threads into the list widget, adding icons for attached files and instructions."""
def load_threads_with_attachments(self, threads):
"""Load threads into the list widget, adding icons for attached files only, based on attachments info."""
for thread in threads:
item = QListWidgetItem(thread['thread_name'])
self.addItem(item)
thread_tooltip_text = f"You can add/remove files and instructions by right-clicking this item."
thread_tooltip_text = "You can add/remove files by right-clicking this item. NOTE: ChatAssistant will not be able to access the files."
item.setToolTip(thread_tooltip_text)

# Get file paths and instructions from the thread data
file_paths = thread.get('file_ids', [])
additional_instructions = thread.get('additional_instructions', "")
# Get attachments from the thread data
attachments = thread.get('attachments', [])

# Update the item with the paperclip icon if there are attached files or instructions
self.update_item_with_files_and_instructions(item, file_paths, additional_instructions)
# Update the item to reflect any attachments
self.update_item_with_attachments(item, attachments)

def update_item_with_files_and_instructions(self, item, file_paths, additional_instructions=""):
"""Update the given item with a paperclip icon if there are attached files or instructions."""
def update_item_with_attachments(self, item, attachments):
"""Update the given item with a paperclip icon if there are attachments."""
row = self.row(item)
if file_paths or additional_instructions:
if attachments:
item.setIcon(QIcon("gui/images/paperclip_icon.png"))
else:
item.setIcon(QIcon())

self.itemToFileMap[row] = file_paths
self.itemToInstructionsMap[row] = additional_instructions
# Store complete attachment information in the mapping
self.itemToFileMap[row] = attachments[:]

def keyPressEvent(self, event):
if event.key() in (Qt.Key_Delete, Qt.Key_Backspace):
Expand Down Expand Up @@ -250,6 +258,12 @@ def __init__(self, main_window):

# Create a list widget for displaying the threads
self.threadList = CustomListWidget(self)
self.threadList.setStyleSheet("QListWidget {"
" border-style: solid;"
" border-width: 1px;"
" border-color: #a0a0a0 #ffffff #ffffff #a0a0a0;" # Light on top and left, dark on bottom and right
" padding: 1px;"
"}")
self.threadList.setFont(QFont("Arial", 11))

# Create connections for the thread and button
Expand All @@ -263,6 +277,12 @@ def __init__(self, main_window):
self.assistantList = QListWidget(self)
self.assistantList.setFont(QFont("Arial", 11))
self.assistantList.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
self.assistantList.setStyleSheet("QListWidget {"
" border-style: solid;"
" border-width: 1px;"
" border-color: #a0a0a0 #ffffff #ffffff #a0a0a0;" # Light on top and left, dark on bottom and right
" padding: 1px;"
"}")
self.assistantList.itemDoubleClicked.connect(self.on_assistant_double_clicked)
self.assistantList.setToolTip("Select assistants to use in the conversation or double-click to edit the selected assistant.")
self.threadList.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
Expand Down Expand Up @@ -317,6 +337,7 @@ def on_assistant_config_submitted(self, assistant_config_json, ai_client_type, a
self.assistant_client_manager.register_client(assistant_client.name, assistant_client)
client_type = AIClientType[ai_client_type]
self.main_window.conversation_sidebar.load_assistant_list(client_type)
self.dialog.update_assistant_combobox()
except Exception as e:
QMessageBox.warning(self.main_window, "Error", f"An error occurred while creating/updating the assistant: {e}")

Expand Down Expand Up @@ -410,18 +431,21 @@ def on_ai_client_type_changed(self, index):

# Clear the existing items in the thread list
self.threadList.clear()
self.threadList.clear_files_and_instructions()
self.threadList.clear_files()

# Get the threads for the selected AI client type
threads_client = ConversationThreadClient.get_instance(self._ai_client_type)
# TODO use ai_client_type to retrieve threads from cloud API
threads = threads_client.get_conversation_threads()
self.threadList.load_threads_with_files_and_instructions(threads)
self.threadList.load_threads_with_attachments(threads)
except Exception as e:
logger.error(f"Error while changing AI client type: {e}")
finally:
self.main_window.set_active_ai_client_type(self._ai_client_type)

def set_attachments_for_selected_thread(self, attachments):
"""Set the attachments for the currently selected item."""
self.threadList.set_attachments_for_selected_item(attachments)

def on_add_thread_button_clicked(self):
"""Handle clicks on the add thread button."""
# Get the selected assistant
Expand Down Expand Up @@ -449,7 +473,7 @@ def create_conversation_thread(self, threads_client : ConversationThreadClient,
logger.debug(f"Total time taken to create a new conversation thread: {end_time - start_time} seconds")
new_item = QListWidgetItem(unique_thread_name)
self.threadList.addItem(new_item)
thread_tooltip_text = f"You can add/remove files and instructions by right-clicking this item."
thread_tooltip_text = f"You can add/remove files by right-clicking this item. NOTE: ChatAssistant will not be able to access the files."
new_item.setToolTip(thread_tooltip_text)

if not is_scheduled_task:
Expand Down
Loading

0 comments on commit 0af2933

Please sign in to comment.