mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
Merge pull request #1733 from ManishMadan2882/main
Attachments: Enhancements , strategy specific to certain LLMs
This commit is contained in:
@@ -44,10 +44,13 @@ class ClassicAgent(BaseAgent):
|
||||
):
|
||||
yield {"answer": resp.message.content}
|
||||
else:
|
||||
completion = self.llm.gen_stream(
|
||||
model=self.gpt_model, messages=messages, tools=self.tools
|
||||
)
|
||||
for line in completion:
|
||||
# completion = self.llm.gen_stream(
|
||||
# model=self.gpt_model, messages=messages, tools=self.tools
|
||||
# )
|
||||
# log type of resp
|
||||
logger.info(f"Response type: {type(resp)}")
|
||||
logger.info(f"Response: {resp}")
|
||||
for line in resp:
|
||||
if isinstance(line, str):
|
||||
yield {"answer": line}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class LLMHandler(ABC):
|
||||
@abstractmethod
|
||||
def handle_response(self, agent, resp, tools_dict, messages, attachments=None, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def prepare_messages_with_attachments(self, agent, messages, attachments=None):
|
||||
"""
|
||||
Prepare messages with attachment content if available.
|
||||
@@ -33,15 +33,53 @@ class LLMHandler(ABC):
|
||||
|
||||
logger.info(f"Preparing messages with {len(attachments)} attachments")
|
||||
|
||||
# Check if the LLM has its own custom attachment handling implementation
|
||||
if hasattr(agent.llm, "prepare_messages_with_attachments") and agent.llm.__class__.__name__ != "BaseLLM":
|
||||
logger.info(f"Using {agent.llm.__class__.__name__}'s own prepare_messages_with_attachments method")
|
||||
return agent.llm.prepare_messages_with_attachments(messages, attachments)
|
||||
supported_types = agent.llm.get_supported_attachment_types()
|
||||
|
||||
# Otherwise, append attachment content to the system prompt
|
||||
supported_attachments = []
|
||||
unsupported_attachments = []
|
||||
|
||||
for attachment in attachments:
|
||||
mime_type = attachment.get('mime_type')
|
||||
if not mime_type:
|
||||
import mimetypes
|
||||
file_path = attachment.get('path')
|
||||
if file_path:
|
||||
mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream'
|
||||
else:
|
||||
unsupported_attachments.append(attachment)
|
||||
continue
|
||||
|
||||
if mime_type in supported_types:
|
||||
supported_attachments.append(attachment)
|
||||
else:
|
||||
unsupported_attachments.append(attachment)
|
||||
|
||||
# Process supported attachments with the LLM's custom method
|
||||
prepared_messages = messages
|
||||
if supported_attachments:
|
||||
logger.info(f"Processing {len(supported_attachments)} supported attachments with {agent.llm.__class__.__name__}'s method")
|
||||
prepared_messages = agent.llm.prepare_messages_with_attachments(messages, supported_attachments)
|
||||
|
||||
# Process unsupported attachments with the default method
|
||||
if unsupported_attachments:
|
||||
logger.info(f"Processing {len(unsupported_attachments)} unsupported attachments with default method")
|
||||
prepared_messages = self._append_attachment_content_to_system(prepared_messages, unsupported_attachments)
|
||||
|
||||
return prepared_messages
|
||||
|
||||
def _append_attachment_content_to_system(self, messages, attachments):
|
||||
"""
|
||||
Default method to append attachment content to the system prompt.
|
||||
|
||||
Args:
|
||||
messages (list): List of message dictionaries.
|
||||
attachments (list): List of attachment dictionaries with content.
|
||||
|
||||
Returns:
|
||||
list: Messages with attachment context added to the system prompt.
|
||||
"""
|
||||
prepared_messages = messages.copy()
|
||||
|
||||
# Build attachment content string
|
||||
attachment_texts = []
|
||||
for attachment in attachments:
|
||||
logger.info(f"Adding attachment {attachment.get('id')} to context")
|
||||
@@ -122,12 +160,13 @@ class OpenAILLMHandler(LLMHandler):
|
||||
return resp
|
||||
|
||||
else:
|
||||
|
||||
text_buffer = ""
|
||||
while True:
|
||||
tool_calls = {}
|
||||
for chunk in resp:
|
||||
if isinstance(chunk, str) and len(chunk) > 0:
|
||||
return
|
||||
yield chunk
|
||||
continue
|
||||
elif hasattr(chunk, "delta"):
|
||||
chunk_delta = chunk.delta
|
||||
|
||||
@@ -206,12 +245,17 @@ class OpenAILLMHandler(LLMHandler):
|
||||
}
|
||||
)
|
||||
tool_calls = {}
|
||||
if hasattr(chunk_delta, "content") and chunk_delta.content:
|
||||
# Add to buffer or yield immediately based on your preference
|
||||
text_buffer += chunk_delta.content
|
||||
yield text_buffer
|
||||
text_buffer = ""
|
||||
|
||||
if (
|
||||
hasattr(chunk, "finish_reason")
|
||||
and chunk.finish_reason == "stop"
|
||||
):
|
||||
return
|
||||
return resp
|
||||
elif isinstance(chunk, str) and len(chunk) == 0:
|
||||
continue
|
||||
|
||||
@@ -227,7 +271,7 @@ class GoogleLLMHandler(LLMHandler):
|
||||
from google.genai import types
|
||||
|
||||
messages = self.prepare_messages_with_attachments(agent, messages, attachments)
|
||||
|
||||
|
||||
while True:
|
||||
if not stream:
|
||||
response = agent.llm.gen(
|
||||
@@ -298,6 +342,9 @@ class GoogleLLMHandler(LLMHandler):
|
||||
"content": [function_response_part.to_json_dict()],
|
||||
}
|
||||
)
|
||||
else:
|
||||
tool_call_found = False
|
||||
yield result
|
||||
|
||||
if not tool_call_found:
|
||||
return response
|
||||
|
||||
@@ -835,12 +835,7 @@ def get_attachments_content(attachment_ids, user):
|
||||
})
|
||||
|
||||
if attachment_doc:
|
||||
attachments.append({
|
||||
"id": str(attachment_doc["_id"]),
|
||||
"content": attachment_doc["content"],
|
||||
"token_count": attachment_doc.get("token_count", 0),
|
||||
"path": attachment_doc.get("path", "")
|
||||
})
|
||||
attachments.append(attachment_doc)
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving attachment {attachment_id}: {e}")
|
||||
|
||||
|
||||
@@ -2506,23 +2506,26 @@ class StoreAttachment(Resource):
|
||||
user = secure_filename(decoded_token.get("sub"))
|
||||
|
||||
try:
|
||||
attachment_id = ObjectId()
|
||||
original_filename = secure_filename(file.filename)
|
||||
folder_name = original_filename
|
||||
save_dir = os.path.join(current_dir, settings.UPLOAD_FOLDER, user, "attachments",folder_name)
|
||||
|
||||
save_dir = os.path.join(
|
||||
current_dir,
|
||||
settings.UPLOAD_FOLDER,
|
||||
user,
|
||||
"attachments",
|
||||
str(attachment_id)
|
||||
)
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
# Create directory structure: user/attachments/filename/
|
||||
|
||||
file_path = os.path.join(save_dir, original_filename)
|
||||
|
||||
# Handle filename conflicts
|
||||
if os.path.exists(file_path):
|
||||
name_parts = os.path.splitext(original_filename)
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
new_filename = f"{name_parts[0]}_{timestamp}{name_parts[1]}"
|
||||
file_path = os.path.join(save_dir, new_filename)
|
||||
original_filename = new_filename
|
||||
|
||||
file.save(file_path)
|
||||
file_info = {"folder": folder_name, "filename": original_filename}
|
||||
file_info = {
|
||||
"filename": original_filename,
|
||||
"attachment_id": str(attachment_id)
|
||||
}
|
||||
current_app.logger.info(f"Saved file: {file_path}")
|
||||
|
||||
# Start async task to process single file
|
||||
|
||||
@@ -55,3 +55,12 @@ class BaseLLM(ABC):
|
||||
|
||||
def _supports_tools(self):
|
||||
raise NotImplementedError("Subclass must implement _supports_tools method")
|
||||
|
||||
def get_supported_attachment_types(self):
|
||||
"""
|
||||
Return a list of MIME types supported by this LLM for file uploads.
|
||||
|
||||
Returns:
|
||||
list: List of supported MIME types
|
||||
"""
|
||||
return [] # Default: no attachments supported
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
import os
|
||||
import logging
|
||||
import mimetypes
|
||||
import json
|
||||
|
||||
from application.llm.base import BaseLLM
|
||||
|
||||
@@ -9,6 +13,138 @@ class GoogleLLM(BaseLLM):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.api_key = api_key
|
||||
self.user_api_key = user_api_key
|
||||
self.client = genai.Client(api_key=self.api_key)
|
||||
|
||||
def get_supported_attachment_types(self):
|
||||
"""
|
||||
Return a list of MIME types supported by Google Gemini for file uploads.
|
||||
|
||||
Returns:
|
||||
list: List of supported MIME types
|
||||
"""
|
||||
return [
|
||||
'application/pdf',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/webp',
|
||||
'image/gif'
|
||||
]
|
||||
|
||||
def prepare_messages_with_attachments(self, messages, attachments=None):
|
||||
"""
|
||||
Process attachments using Google AI's file API for more efficient handling.
|
||||
|
||||
Args:
|
||||
messages (list): List of message dictionaries.
|
||||
attachments (list): List of attachment dictionaries with content and metadata.
|
||||
|
||||
Returns:
|
||||
list: Messages formatted with file references for Google AI API.
|
||||
"""
|
||||
if not attachments:
|
||||
return messages
|
||||
|
||||
prepared_messages = messages.copy()
|
||||
|
||||
# Find the user message to attach files to the last one
|
||||
user_message_index = None
|
||||
for i in range(len(prepared_messages) - 1, -1, -1):
|
||||
if prepared_messages[i].get("role") == "user":
|
||||
user_message_index = i
|
||||
break
|
||||
|
||||
if user_message_index is None:
|
||||
user_message = {"role": "user", "content": []}
|
||||
prepared_messages.append(user_message)
|
||||
user_message_index = len(prepared_messages) - 1
|
||||
|
||||
if isinstance(prepared_messages[user_message_index].get("content"), str):
|
||||
text_content = prepared_messages[user_message_index]["content"]
|
||||
prepared_messages[user_message_index]["content"] = [
|
||||
{"type": "text", "text": text_content}
|
||||
]
|
||||
elif not isinstance(prepared_messages[user_message_index].get("content"), list):
|
||||
prepared_messages[user_message_index]["content"] = []
|
||||
|
||||
files = []
|
||||
for attachment in attachments:
|
||||
mime_type = attachment.get('mime_type')
|
||||
if not mime_type:
|
||||
file_path = attachment.get('path')
|
||||
if file_path:
|
||||
mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream'
|
||||
|
||||
if mime_type in self.get_supported_attachment_types():
|
||||
try:
|
||||
file_uri = self._upload_file_to_google(attachment)
|
||||
logging.info(f"GoogleLLM: Successfully uploaded file, got URI: {file_uri}")
|
||||
files.append({"file_uri": file_uri, "mime_type": mime_type})
|
||||
except Exception as e:
|
||||
logging.error(f"GoogleLLM: Error uploading file: {e}")
|
||||
if 'content' in attachment:
|
||||
prepared_messages[user_message_index]["content"].append({
|
||||
"type": "text",
|
||||
"text": f"[File could not be processed: {attachment.get('path', 'unknown')}]"
|
||||
})
|
||||
|
||||
if files:
|
||||
logging.info(f"GoogleLLM: Adding {len(files)} files to message")
|
||||
prepared_messages[user_message_index]["content"].append({
|
||||
"files": files
|
||||
})
|
||||
|
||||
return prepared_messages
|
||||
|
||||
def _upload_file_to_google(self, attachment):
|
||||
"""
|
||||
Upload a file to Google AI and return the file URI.
|
||||
|
||||
Args:
|
||||
attachment (dict): Attachment dictionary with path and metadata.
|
||||
|
||||
Returns:
|
||||
str: Google AI file URI for the uploaded file.
|
||||
"""
|
||||
if 'google_file_uri' in attachment:
|
||||
return attachment['google_file_uri']
|
||||
|
||||
file_path = attachment.get('path')
|
||||
if not file_path:
|
||||
raise ValueError("No file path provided in attachment")
|
||||
|
||||
if not os.path.isabs(file_path):
|
||||
current_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
file_path = os.path.join(current_dir, "application", file_path)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"File not found: {file_path}")
|
||||
|
||||
mime_type = attachment.get('mime_type')
|
||||
if not mime_type:
|
||||
mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream'
|
||||
|
||||
try:
|
||||
response = self.client.files.upload(file=file_path)
|
||||
|
||||
file_uri = response.uri
|
||||
|
||||
from application.core.mongo_db import MongoDB
|
||||
mongo = MongoDB.get_client()
|
||||
db = mongo["docsgpt"]
|
||||
attachments_collection = db["attachments"]
|
||||
if '_id' in attachment:
|
||||
attachments_collection.update_one(
|
||||
{"_id": attachment['_id']},
|
||||
{"$set": {"google_file_uri": file_uri}}
|
||||
)
|
||||
|
||||
return file_uri
|
||||
except Exception as e:
|
||||
logging.error(f"Error uploading file to Google AI: {e}")
|
||||
raise
|
||||
|
||||
def _clean_messages_google(self, messages):
|
||||
cleaned_messages = []
|
||||
@@ -26,7 +162,7 @@ class GoogleLLM(BaseLLM):
|
||||
elif isinstance(content, list):
|
||||
for item in content:
|
||||
if "text" in item:
|
||||
parts.append(types.Part.from_text(item["text"]))
|
||||
parts.append(types.Part.from_text(text=item["text"]))
|
||||
elif "function_call" in item:
|
||||
parts.append(
|
||||
types.Part.from_function_call(
|
||||
@@ -41,6 +177,14 @@ class GoogleLLM(BaseLLM):
|
||||
response=item["function_response"]["response"],
|
||||
)
|
||||
)
|
||||
elif "files" in item:
|
||||
for file_data in item["files"]:
|
||||
parts.append(
|
||||
types.Part.from_uri(
|
||||
file_uri=file_data["file_uri"],
|
||||
mime_type=file_data["mime_type"]
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unexpected content dictionary format:{item}"
|
||||
@@ -145,12 +289,26 @@ class GoogleLLM(BaseLLM):
|
||||
if tools:
|
||||
cleaned_tools = self._clean_tools_format(tools)
|
||||
config.tools = cleaned_tools
|
||||
|
||||
|
||||
# Check if we have both tools and file attachments
|
||||
has_attachments = False
|
||||
for message in messages:
|
||||
for part in message.parts:
|
||||
if hasattr(part, 'file_data') and part.file_data is not None:
|
||||
has_attachments = True
|
||||
break
|
||||
if has_attachments:
|
||||
break
|
||||
|
||||
logging.info(f"GoogleLLM: Starting stream generation. Model: {model}, Messages: {json.dumps(messages, default=str)}, Has attachments: {has_attachments}")
|
||||
|
||||
response = client.models.generate_content_stream(
|
||||
model=model,
|
||||
contents=messages,
|
||||
config=config,
|
||||
)
|
||||
|
||||
|
||||
for chunk in response:
|
||||
if hasattr(chunk, "candidates") and chunk.candidates:
|
||||
for candidate in chunk.candidates:
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import json
|
||||
import base64
|
||||
import os
|
||||
import mimetypes
|
||||
import logging
|
||||
|
||||
from application.core.settings import settings
|
||||
from application.llm.base import BaseLLM
|
||||
@@ -65,6 +69,15 @@ class OpenAILLM(BaseLLM):
|
||||
),
|
||||
}
|
||||
)
|
||||
elif isinstance(item, dict):
|
||||
content_parts = []
|
||||
if "text" in item:
|
||||
content_parts.append({"type": "text", "text": item["text"]})
|
||||
elif "type" in item and item["type"] == "text" and "text" in item:
|
||||
content_parts.append(item)
|
||||
elif "type" in item and item["type"] == "file" and "file" in item:
|
||||
content_parts.append(item)
|
||||
cleaned_messages.append({"role": role, "content": content_parts})
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unexpected content dictionary format: {item}"
|
||||
@@ -133,6 +146,183 @@ class OpenAILLM(BaseLLM):
|
||||
def _supports_tools(self):
|
||||
return True
|
||||
|
||||
def get_supported_attachment_types(self):
|
||||
"""
|
||||
Return a list of MIME types supported by OpenAI for file uploads.
|
||||
|
||||
Returns:
|
||||
list: List of supported MIME types
|
||||
"""
|
||||
return [
|
||||
'application/pdf',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/webp',
|
||||
'image/gif'
|
||||
]
|
||||
|
||||
def prepare_messages_with_attachments(self, messages, attachments=None):
|
||||
"""
|
||||
Process attachments using OpenAI's file API for more efficient handling.
|
||||
|
||||
Args:
|
||||
messages (list): List of message dictionaries.
|
||||
attachments (list): List of attachment dictionaries with content and metadata.
|
||||
|
||||
Returns:
|
||||
list: Messages formatted with file references for OpenAI API.
|
||||
"""
|
||||
if not attachments:
|
||||
return messages
|
||||
|
||||
prepared_messages = messages.copy()
|
||||
|
||||
# Find the user message to attach file_id to the last one
|
||||
user_message_index = None
|
||||
for i in range(len(prepared_messages) - 1, -1, -1):
|
||||
if prepared_messages[i].get("role") == "user":
|
||||
user_message_index = i
|
||||
break
|
||||
|
||||
if user_message_index is None:
|
||||
user_message = {"role": "user", "content": []}
|
||||
prepared_messages.append(user_message)
|
||||
user_message_index = len(prepared_messages) - 1
|
||||
|
||||
if isinstance(prepared_messages[user_message_index].get("content"), str):
|
||||
text_content = prepared_messages[user_message_index]["content"]
|
||||
prepared_messages[user_message_index]["content"] = [
|
||||
{"type": "text", "text": text_content}
|
||||
]
|
||||
elif not isinstance(prepared_messages[user_message_index].get("content"), list):
|
||||
prepared_messages[user_message_index]["content"] = []
|
||||
|
||||
for attachment in attachments:
|
||||
mime_type = attachment.get('mime_type')
|
||||
if not mime_type:
|
||||
file_path = attachment.get('path')
|
||||
if file_path:
|
||||
mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream'
|
||||
|
||||
if mime_type and mime_type.startswith('image/'):
|
||||
try:
|
||||
base64_image = self._get_base64_image(attachment)
|
||||
prepared_messages[user_message_index]["content"].append({
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": f"data:{mime_type};base64,{base64_image}"
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing image attachment: {e}")
|
||||
if 'content' in attachment:
|
||||
prepared_messages[user_message_index]["content"].append({
|
||||
"type": "text",
|
||||
"text": f"[Image could not be processed: {attachment.get('path', 'unknown')}]"
|
||||
})
|
||||
# Handle PDFs using the file API
|
||||
elif mime_type == 'application/pdf':
|
||||
try:
|
||||
file_id = self._upload_file_to_openai(attachment)
|
||||
|
||||
prepared_messages[user_message_index]["content"].append({
|
||||
"type": "file",
|
||||
"file": {"file_id": file_id}
|
||||
})
|
||||
except Exception as e:
|
||||
logging.error(f"Error uploading PDF to OpenAI: {e}")
|
||||
if 'content' in attachment:
|
||||
prepared_messages[user_message_index]["content"].append({
|
||||
"type": "text",
|
||||
"text": f"File content:\n\n{attachment['content']}"
|
||||
})
|
||||
|
||||
return prepared_messages
|
||||
|
||||
def _get_base64_image(self, attachment):
|
||||
"""
|
||||
Convert an image file to base64 encoding.
|
||||
|
||||
Args:
|
||||
attachment (dict): Attachment dictionary with path and metadata.
|
||||
|
||||
Returns:
|
||||
str: Base64-encoded image data.
|
||||
"""
|
||||
file_path = attachment.get('path')
|
||||
if not file_path:
|
||||
raise ValueError("No file path provided in attachment")
|
||||
|
||||
if not os.path.isabs(file_path):
|
||||
current_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
file_path = os.path.join(current_dir, "application", file_path)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"File not found: {file_path}")
|
||||
|
||||
with open(file_path, "rb") as image_file:
|
||||
return base64.b64encode(image_file.read()).decode('utf-8')
|
||||
|
||||
def _upload_file_to_openai(self, attachment): ##pdfs
|
||||
"""
|
||||
Upload a file to OpenAI and return the file_id.
|
||||
|
||||
Args:
|
||||
attachment (dict): Attachment dictionary with path and metadata.
|
||||
Expected keys:
|
||||
- path: Path to the file
|
||||
- id: Optional MongoDB ID for caching
|
||||
|
||||
Returns:
|
||||
str: OpenAI file_id for the uploaded file.
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
|
||||
if 'openai_file_id' in attachment:
|
||||
return attachment['openai_file_id']
|
||||
|
||||
file_path = attachment.get('path')
|
||||
if not file_path:
|
||||
raise ValueError("No file path provided in attachment")
|
||||
|
||||
if not os.path.isabs(file_path):
|
||||
current_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
file_path = os.path.join(current_dir,"application", file_path)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"File not found: {file_path}")
|
||||
|
||||
|
||||
try:
|
||||
with open(file_path, 'rb') as file:
|
||||
response = self.client.files.create(
|
||||
file=file,
|
||||
purpose="assistants"
|
||||
)
|
||||
|
||||
file_id = response.id
|
||||
|
||||
from application.core.mongo_db import MongoDB
|
||||
mongo = MongoDB.get_client()
|
||||
db = mongo["docsgpt"]
|
||||
attachments_collection = db["attachments"]
|
||||
if '_id' in attachment:
|
||||
attachments_collection.update_one(
|
||||
{"_id": attachment['_id']},
|
||||
{"$set": {"openai_file_id": file_id}}
|
||||
)
|
||||
|
||||
return file_id
|
||||
except Exception as e:
|
||||
logging.error(f"Error uploading file to OpenAI: {e}")
|
||||
raise
|
||||
|
||||
|
||||
class AzureOpenAILLM(OpenAILLM):
|
||||
|
||||
|
||||
@@ -328,34 +328,30 @@ def attachment_worker(self, directory, file_info, user):
|
||||
"""
|
||||
import datetime
|
||||
import os
|
||||
import mimetypes
|
||||
from application.utils import num_tokens_from_string
|
||||
|
||||
mongo = MongoDB.get_client()
|
||||
db = mongo["docsgpt"]
|
||||
attachments_collection = db["attachments"]
|
||||
|
||||
job_name = file_info["folder"]
|
||||
logging.info(f"Processing attachment: {job_name}", extra={"user": user, "job": job_name})
|
||||
filename = file_info["filename"]
|
||||
attachment_id = file_info["attachment_id"]
|
||||
|
||||
logging.info(f"Processing attachment: {attachment_id}/{filename}", extra={"user": user})
|
||||
|
||||
self.update_state(state="PROGRESS", meta={"current": 10})
|
||||
|
||||
folder_name = file_info["folder"]
|
||||
filename = file_info["filename"]
|
||||
|
||||
file_path = os.path.join(directory, filename)
|
||||
|
||||
|
||||
logging.info(f"Processing file: {file_path}", extra={"user": user, "job": job_name})
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
logging.warning(f"File not found: {file_path}", extra={"user": user, "job": job_name})
|
||||
return {"error": "File not found"}
|
||||
logging.warning(f"File not found: {file_path}", extra={"user": user})
|
||||
raise FileNotFoundError(f"File not found: {file_path}")
|
||||
|
||||
try:
|
||||
reader = SimpleDirectoryReader(
|
||||
input_files=[file_path]
|
||||
)
|
||||
|
||||
documents = reader.load_data()
|
||||
|
||||
self.update_state(state="PROGRESS", meta={"current": 50})
|
||||
@@ -364,33 +360,37 @@ def attachment_worker(self, directory, file_info, user):
|
||||
content = documents[0].text
|
||||
token_count = num_tokens_from_string(content)
|
||||
|
||||
file_path_relative = f"{user}/attachments/{folder_name}/{filename}"
|
||||
file_path_relative = f"{settings.UPLOAD_FOLDER}/{user}/attachments/{attachment_id}/{filename}"
|
||||
|
||||
attachment_id = attachments_collection.insert_one({
|
||||
mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream'
|
||||
|
||||
doc_id = ObjectId(attachment_id)
|
||||
attachments_collection.insert_one({
|
||||
"_id": doc_id,
|
||||
"user": user,
|
||||
"path": file_path_relative,
|
||||
"content": content,
|
||||
"token_count": token_count,
|
||||
"mime_type": mime_type,
|
||||
"date": datetime.datetime.now(),
|
||||
}).inserted_id
|
||||
})
|
||||
|
||||
logging.info(f"Stored attachment with ID: {attachment_id}",
|
||||
extra={"user": user, "job": job_name})
|
||||
extra={"user": user})
|
||||
|
||||
self.update_state(state="PROGRESS", meta={"current": 100})
|
||||
|
||||
return {
|
||||
"attachment_id": str(attachment_id),
|
||||
"filename": filename,
|
||||
"folder": folder_name,
|
||||
"path": file_path_relative,
|
||||
"token_count": token_count
|
||||
"token_count": token_count,
|
||||
"attachment_id": attachment_id,
|
||||
"mime_type": mime_type
|
||||
}
|
||||
else:
|
||||
logging.warning("No content was extracted from the file",
|
||||
extra={"user": user, "job": job_name})
|
||||
return {"error": "No content was extracted from the file"}
|
||||
extra={"user": user})
|
||||
raise ValueError("No content was extracted from the file")
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing file {filename}: {e}",
|
||||
extra={"user": user, "job": job_name}, exc_info=True)
|
||||
return {"error": f"Error processing file: {str(e)}"}
|
||||
logging.error(f"Error processing file {filename}: {e}", extra={"user": user}, exc_info=True)
|
||||
raise
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef,useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import userService from '../api/services/userService';
|
||||
import endpoints from '../api/endpoints';
|
||||
import { getOS, isTouchDevice } from '../utils/browserUtils';
|
||||
import PaperPlane from '../assets/paper_plane.svg';
|
||||
import SourceIcon from '../assets/source.svg';
|
||||
import ToolIcon from '../assets/tool.svg';
|
||||
import SpinnerDark from '../assets/spinner-dark.svg';
|
||||
import Spinner from '../assets/spinner.svg';
|
||||
import ExitIcon from '../assets/exit.svg';
|
||||
import AlertIcon from '../assets/alert.svg';
|
||||
import SourcesPopup from './SourcesPopup';
|
||||
import ToolsPopup from './ToolsPopup';
|
||||
import { selectSelectedDocs, selectToken } from '../preferences/preferenceSlice';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import Upload from '../upload/Upload';
|
||||
import ClipIcon from '../assets/clip.svg';
|
||||
import { setAttachments } from '../conversation/conversationSlice';
|
||||
import {
|
||||
addAttachment,
|
||||
updateAttachment,
|
||||
removeAttachment,
|
||||
selectAttachments
|
||||
} from '../conversation/conversationSlice';
|
||||
|
||||
|
||||
interface MessageInputProps {
|
||||
value: string;
|
||||
@@ -47,13 +56,33 @@ export default function MessageInput({
|
||||
const [isSourcesPopupOpen, setIsSourcesPopupOpen] = useState(false);
|
||||
const [isToolsPopupOpen, setIsToolsPopupOpen] = useState(false);
|
||||
const [uploadModalState, setUploadModalState] = useState<ActiveState>('INACTIVE');
|
||||
const [uploads, setUploads] = useState<UploadState[]>([]);
|
||||
|
||||
const selectedDocs = useSelector(selectSelectedDocs);
|
||||
const token = useSelector(selectToken);
|
||||
const attachments = useSelector(selectAttachments);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const browserOS = getOS();
|
||||
const isTouch = isTouchDevice();
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
((browserOS === 'win' || browserOS === 'linux') && event.ctrlKey && event.key === 'k') ||
|
||||
(browserOS === 'mac' && event.metaKey && event.key === 'k')
|
||||
) {
|
||||
event.preventDefault();
|
||||
setIsSourcesPopupOpen(!isSourcesPopupOpen);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [browserOS]);
|
||||
|
||||
const handleFileAttachment = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files || e.target.files.length === 0) return;
|
||||
|
||||
@@ -64,56 +93,51 @@ export default function MessageInput({
|
||||
const apiHost = import.meta.env.VITE_API_HOST;
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
const uploadState: UploadState = {
|
||||
taskId: '',
|
||||
const newAttachment = {
|
||||
fileName: file.name,
|
||||
progress: 0,
|
||||
status: 'uploading'
|
||||
status: 'uploading' as const,
|
||||
taskId: '',
|
||||
};
|
||||
|
||||
setUploads(prev => [...prev, uploadState]);
|
||||
const uploadIndex = uploads.length;
|
||||
dispatch(addAttachment(newAttachment));
|
||||
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
if (event.lengthComputable) {
|
||||
const progress = Math.round((event.loaded / event.total) * 100);
|
||||
setUploads(prev => prev.map((upload, index) =>
|
||||
index === uploadIndex
|
||||
? { ...upload, progress }
|
||||
: upload
|
||||
));
|
||||
dispatch(updateAttachment({
|
||||
taskId: newAttachment.taskId,
|
||||
updates: { progress }
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
console.log('File uploaded successfully:', response);
|
||||
|
||||
if (response.task_id) {
|
||||
setUploads(prev => prev.map((upload, index) =>
|
||||
index === uploadIndex
|
||||
? { ...upload, taskId: response.task_id, status: 'processing' }
|
||||
: upload
|
||||
));
|
||||
dispatch(updateAttachment({
|
||||
taskId: newAttachment.taskId,
|
||||
updates: {
|
||||
taskId: response.task_id,
|
||||
status: 'processing',
|
||||
progress: 10
|
||||
}
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
setUploads(prev => prev.map((upload, index) =>
|
||||
index === uploadIndex
|
||||
? { ...upload, status: 'failed' }
|
||||
: upload
|
||||
));
|
||||
console.error('Error uploading file:', xhr.responseText);
|
||||
dispatch(updateAttachment({
|
||||
taskId: newAttachment.taskId,
|
||||
updates: { status: 'failed' }
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
setUploads(prev => prev.map((upload, index) =>
|
||||
index === uploadIndex
|
||||
? { ...upload, status: 'failed' }
|
||||
: upload
|
||||
));
|
||||
console.error('Network error during file upload');
|
||||
dispatch(updateAttachment({
|
||||
taskId: newAttachment.taskId,
|
||||
updates: { status: 'failed' }
|
||||
}));
|
||||
};
|
||||
|
||||
xhr.open('POST', `${apiHost}${endpoints.USER.STORE_ATTACHMENT}`);
|
||||
@@ -123,64 +147,55 @@ export default function MessageInput({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutIds: number[] = [];
|
||||
|
||||
const checkTaskStatus = () => {
|
||||
const processingUploads = uploads.filter(upload =>
|
||||
upload.status === 'processing' && upload.taskId
|
||||
const processingAttachments = attachments.filter(att =>
|
||||
att.status === 'processing' && att.taskId
|
||||
);
|
||||
|
||||
processingUploads.forEach(upload => {
|
||||
processingAttachments.forEach(attachment => {
|
||||
userService
|
||||
.getTaskStatus(upload.taskId, null)
|
||||
.getTaskStatus(attachment.taskId!, null)
|
||||
.then((data) => data.json())
|
||||
.then((data) => {
|
||||
console.log('Task status:', data);
|
||||
|
||||
setUploads(prev => prev.map(u => {
|
||||
if (u.taskId !== upload.taskId) return u;
|
||||
|
||||
if (data.status === 'SUCCESS') {
|
||||
return {
|
||||
...u,
|
||||
if (data.status === 'SUCCESS') {
|
||||
dispatch(updateAttachment({
|
||||
taskId: attachment.taskId!,
|
||||
updates: {
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
attachment_id: data.result?.attachment_id,
|
||||
id: data.result?.attachment_id,
|
||||
token_count: data.result?.token_count
|
||||
};
|
||||
} else if (data.status === 'FAILURE') {
|
||||
return { ...u, status: 'failed' };
|
||||
} else if (data.status === 'PROGRESS' && data.result?.current) {
|
||||
return { ...u, progress: data.result.current };
|
||||
}
|
||||
return u;
|
||||
}));
|
||||
|
||||
if (data.status !== 'SUCCESS' && data.status !== 'FAILURE') {
|
||||
const timeoutId = window.setTimeout(() => checkTaskStatus(), 2000);
|
||||
timeoutIds.push(timeoutId);
|
||||
}
|
||||
}));
|
||||
} else if (data.status === 'FAILURE') {
|
||||
dispatch(updateAttachment({
|
||||
taskId: attachment.taskId!,
|
||||
updates: { status: 'failed' }
|
||||
}));
|
||||
} else if (data.status === 'PROGRESS' && data.result?.current) {
|
||||
dispatch(updateAttachment({
|
||||
taskId: attachment.taskId!,
|
||||
updates: { progress: data.result.current }
|
||||
}));
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error checking task status:', error);
|
||||
setUploads(prev => prev.map(u =>
|
||||
u.taskId === upload.taskId
|
||||
? { ...u, status: 'failed' }
|
||||
: u
|
||||
));
|
||||
.catch(() => {
|
||||
dispatch(updateAttachment({
|
||||
taskId: attachment.taskId!,
|
||||
updates: { status: 'failed' }
|
||||
}));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (uploads.some(upload => upload.status === 'processing')) {
|
||||
const timeoutId = window.setTimeout(checkTaskStatus, 2000);
|
||||
timeoutIds.push(timeoutId);
|
||||
}
|
||||
const interval = setInterval(() => {
|
||||
if (attachments.some(att => att.status === 'processing')) {
|
||||
checkTaskStatus();
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => {
|
||||
timeoutIds.forEach(id => clearTimeout(id));
|
||||
};
|
||||
}, [uploads]);
|
||||
return () => clearInterval(interval);
|
||||
}, [attachments, dispatch]);
|
||||
|
||||
const handleInput = () => {
|
||||
if (inputRef.current) {
|
||||
@@ -215,39 +230,53 @@ export default function MessageInput({
|
||||
|
||||
|
||||
const handleSubmit = () => {
|
||||
const completedAttachments = uploads
|
||||
.filter(upload => upload.status === 'completed' && upload.attachment_id)
|
||||
.map(upload => ({
|
||||
fileName: upload.fileName,
|
||||
id: upload.attachment_id as string
|
||||
}));
|
||||
|
||||
dispatch(setAttachments(completedAttachments));
|
||||
|
||||
onSubmit();
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col w-full mx-2">
|
||||
<div className="flex flex-col w-full rounded-[23px] border dark:border-grey border-dark-gray bg-lotion dark:bg-transparent relative">
|
||||
<div className="flex flex-wrap gap-1.5 sm:gap-2 px-4 sm:px-6 pt-3 pb-0">
|
||||
{uploads.map((upload, index) => (
|
||||
{attachments.map((attachment, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center px-2 sm:px-3 py-1 sm:py-1.5 rounded-[32px] border border-[#AAAAAA] dark:border-purple-taupe bg-white dark:bg-[#1F2028] text-[12px] sm:text-[14px] text-[#5D5D5D] dark:text-bright-gray"
|
||||
className={`flex items-center px-2 sm:px-3 py-1 sm:py-1.5 rounded-[32px] border border-[#AAAAAA] dark:border-purple-taupe bg-white dark:bg-[#1F2028] text-[12px] sm:text-[14px] text-[#5D5D5D] dark:text-bright-gray group relative ${
|
||||
attachment.status !== 'completed' ? 'opacity-70' : 'opacity-100'
|
||||
}`}
|
||||
title={attachment.fileName}
|
||||
>
|
||||
<span className="font-medium truncate max-w-[120px] sm:max-w-[150px]">{upload.fileName}</span>
|
||||
<span className="font-medium truncate max-w-[120px] sm:max-w-[150px]">{attachment.fileName}</span>
|
||||
|
||||
{upload.status === 'completed' && (
|
||||
<span className="ml-2 text-green-500">✓</span>
|
||||
{attachment.status === 'completed' && (
|
||||
<button
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity bg-white dark:bg-[#1F2028] rounded-full p-1 hover:bg-white/95 dark:hover:bg-[#1F2028]/95"
|
||||
onClick={() => {
|
||||
if (attachment.id) {
|
||||
dispatch(removeAttachment(attachment.id));
|
||||
}
|
||||
}}
|
||||
aria-label="Remove attachment"
|
||||
>
|
||||
<img
|
||||
src={ExitIcon}
|
||||
alt="Remove"
|
||||
className="w-2.5 h-2.5 filter dark:invert"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{upload.status === 'failed' && (
|
||||
<span className="ml-2 text-red-500">✗</span>
|
||||
|
||||
{attachment.status === 'failed' && (
|
||||
<img
|
||||
src={AlertIcon}
|
||||
alt="Upload failed"
|
||||
className="ml-2 w-3.5 h-3.5"
|
||||
title="Upload failed"
|
||||
/>
|
||||
)}
|
||||
|
||||
{(upload.status === 'uploading' || upload.status === 'processing') && (
|
||||
|
||||
{(attachment.status === 'uploading' || attachment.status === 'processing') && (
|
||||
<div className="ml-2 w-4 h-4 relative">
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24">
|
||||
{/* Background circle */}
|
||||
<circle
|
||||
className="text-gray-200 dark:text-gray-700"
|
||||
cx="12"
|
||||
@@ -266,7 +295,7 @@ export default function MessageInput({
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
strokeDasharray="62.83"
|
||||
strokeDashoffset={62.83 - (upload.progress / 100) * 62.83}
|
||||
strokeDashoffset={62.83 * (1 - attachment.progress / 100)}
|
||||
transform="rotate(-90 12 12)"
|
||||
/>
|
||||
</svg>
|
||||
@@ -298,15 +327,21 @@ export default function MessageInput({
|
||||
<div className="flex-grow flex flex-wrap gap-1 sm:gap-2">
|
||||
<button
|
||||
ref={sourceButtonRef}
|
||||
className="flex items-center px-2 xs:px-3 py-1 xs:py-1.5 rounded-[32px] border border-[#AAAAAA] dark:border-purple-taupe hover:bg-gray-100 dark:hover:bg-[#2C2E3C] transition-colors max-w-[130px] xs:max-w-[150px]"
|
||||
className="flex items-center px-2 xs:px-3 py-1 xs:py-1.5 rounded-[32px] border border-[#AAAAAA] dark:border-purple-taupe hover:bg-gray-100 dark:hover:bg-[#2C2E3C] transition-colors max-w-[130px] sm:max-w-[150px]"
|
||||
onClick={() => setIsSourcesPopupOpen(!isSourcesPopupOpen)}
|
||||
title={selectedDocs ? selectedDocs.name : t('conversation.sources.title')}
|
||||
>
|
||||
<img src={SourceIcon} alt="Sources" className="w-3.5 sm:w-4 h-3.5 sm:h-4 mr-1 sm:mr-1.5 flex-shrink-0" />
|
||||
<img src={SourceIcon} alt="Sources" className="w-3.5 h-3.5 sm:h-4 mr-1 sm:mr-1.5 flex-shrink-0" />
|
||||
<span className="text-[10px] xs:text-[12px] sm:text-[14px] text-[#5D5D5D] dark:text-bright-gray font-medium truncate overflow-hidden">
|
||||
{selectedDocs
|
||||
? selectedDocs.name
|
||||
: t('conversation.sources.title')}
|
||||
</span>
|
||||
{!isTouch && (
|
||||
<span className="hidden sm:inline-block ml-1 text-[10px] text-gray-500 dark:text-gray-400">
|
||||
{browserOS === 'mac' ? '(⌘K)' : '(ctrl+K)'}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
||||
@@ -207,7 +207,7 @@ export default function SourcesPopup({
|
||||
<div className="px-4 md:px-6 py-4 opacity-75 hover:opacity-100 transition-opacity duration-200 flex-shrink-0">
|
||||
<a
|
||||
href="/settings/documents"
|
||||
className="text-violets-are-blue text-base font-medium flex items-center gap-2"
|
||||
className="text-violets-are-blue text-base font-medium inline-flex items-center gap-2"
|
||||
onClick={onClose}
|
||||
>
|
||||
Go to Documents
|
||||
|
||||
@@ -217,10 +217,10 @@ export default function ToolsPopup({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4 flex-shrink-0">
|
||||
<div className="p-4 flex-shrink-0 opacity-75 hover:opacity-100 transition-opacity duration-200">
|
||||
<a
|
||||
href="/settings/tools"
|
||||
className="text-base text-purple-30 font-medium hover:text-violets-are-blue flex items-center"
|
||||
className="text-base text-purple-30 font-medium inline-flex items-center"
|
||||
>
|
||||
{t('settings.tools.manageTools')}
|
||||
<img
|
||||
@@ -233,4 +233,4 @@ export default function ToolsPopup({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ const ConversationBubble = forwardRef<
|
||||
updated?: boolean,
|
||||
index?: number,
|
||||
) => void;
|
||||
attachments?: { fileName: string; id: string }[];
|
||||
}
|
||||
>(function ConversationBubble(
|
||||
{
|
||||
@@ -72,7 +71,6 @@ const ConversationBubble = forwardRef<
|
||||
retryBtn,
|
||||
questionNumber,
|
||||
handleUpdatedQuestionSubmission,
|
||||
attachments,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
@@ -99,36 +97,6 @@ const ConversationBubble = forwardRef<
|
||||
handleUpdatedQuestionSubmission?.(editInputBox, true, questionNumber);
|
||||
};
|
||||
let bubble;
|
||||
const renderAttachments = () => {
|
||||
if (!attachments || attachments.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{attachments.map((attachment, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center rounded-md bg-gray-100 px-2 py-1 text-sm dark:bg-gray-700"
|
||||
>
|
||||
<svg
|
||||
className="mr-1 h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"
|
||||
/>
|
||||
</svg>
|
||||
<span>{attachment.fileName}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
if (type === 'QUESTION') {
|
||||
bubble = (
|
||||
<div
|
||||
@@ -157,7 +125,6 @@ const ConversationBubble = forwardRef<
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
{renderAttachments()}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
@@ -161,7 +161,6 @@ export default function ConversationMessages({
|
||||
{queries.length > 0 ? (
|
||||
queries.map((query, index) => (
|
||||
<Fragment key={index}>
|
||||
|
||||
<ConversationBubble
|
||||
className={'first:mt-5'}
|
||||
key={`${index}QUESTION`}
|
||||
@@ -170,7 +169,6 @@ export default function ConversationMessages({
|
||||
handleUpdatedQuestionSubmission={handleQuestionSubmission}
|
||||
questionNumber={index}
|
||||
sources={query.sources}
|
||||
attachments={query.attachments}
|
||||
/>
|
||||
{prepResponseView(query, index)}
|
||||
</Fragment>
|
||||
|
||||
@@ -9,11 +9,21 @@ export interface Message {
|
||||
type: MESSAGE_TYPE;
|
||||
}
|
||||
|
||||
|
||||
export interface Attachment {
|
||||
id?: string;
|
||||
fileName: string;
|
||||
status: 'uploading' | 'processing' | 'completed' | 'failed';
|
||||
progress: number;
|
||||
taskId?: string;
|
||||
token_count?: number;
|
||||
}
|
||||
|
||||
export interface ConversationState {
|
||||
queries: Query[];
|
||||
status: Status;
|
||||
conversationId: string | null;
|
||||
attachments?: { fileName: string; id: string }[];
|
||||
attachments: Attachment[];
|
||||
}
|
||||
|
||||
export interface Answer {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
handleFetchAnswer,
|
||||
handleFetchAnswerSteaming,
|
||||
} from './conversationHandlers';
|
||||
import { Answer, ConversationState, Query, Status } from './conversationModels';
|
||||
import { Answer, Query, Status, ConversationState, Attachment } from './conversationModels';
|
||||
|
||||
const initialState: ConversationState = {
|
||||
queries: [],
|
||||
@@ -38,7 +38,9 @@ export const fetchAnswer = createAsyncThunk<
|
||||
|
||||
let isSourceUpdated = false;
|
||||
const state = getState() as RootState;
|
||||
const attachments = state.conversation.attachments?.map(a => a.id) || [];
|
||||
const attachmentIds = state.conversation.attachments
|
||||
.filter(a => a.id && a.status === 'completed')
|
||||
.map(a => a.id) as string[];
|
||||
|
||||
if (state.preference) {
|
||||
if (API_STREAMING) {
|
||||
@@ -122,7 +124,7 @@ export const fetchAnswer = createAsyncThunk<
|
||||
}
|
||||
},
|
||||
indx,
|
||||
attachments
|
||||
attachmentIds
|
||||
);
|
||||
} else {
|
||||
const answer = await handleFetchAnswer(
|
||||
@@ -135,7 +137,7 @@ export const fetchAnswer = createAsyncThunk<
|
||||
state.preference.prompt.id,
|
||||
state.preference.chunks,
|
||||
state.preference.token_limit,
|
||||
attachments
|
||||
attachmentIds
|
||||
);
|
||||
if (answer) {
|
||||
let sourcesPrepped = [];
|
||||
@@ -286,9 +288,29 @@ export const conversationSlice = createSlice({
|
||||
const { index, message } = action.payload;
|
||||
state.queries[index].error = message;
|
||||
},
|
||||
setAttachments: (state, action: PayloadAction<{ fileName: string; id: string }[]>) => {
|
||||
setAttachments: (state, action: PayloadAction<Attachment[]>) => {
|
||||
state.attachments = action.payload;
|
||||
},
|
||||
addAttachment: (state, action: PayloadAction<Attachment>) => {
|
||||
state.attachments.push(action.payload);
|
||||
},
|
||||
updateAttachment: (state, action: PayloadAction<{
|
||||
taskId: string;
|
||||
updates: Partial<Attachment>;
|
||||
}>) => {
|
||||
const index = state.attachments.findIndex(att => att.taskId === action.payload.taskId);
|
||||
if (index !== -1) {
|
||||
state.attachments[index] = {
|
||||
...state.attachments[index],
|
||||
...action.payload.updates
|
||||
};
|
||||
}
|
||||
},
|
||||
removeAttachment: (state, action: PayloadAction<string>) => {
|
||||
state.attachments = state.attachments.filter(att =>
|
||||
att.taskId !== action.payload && att.id !== action.payload
|
||||
);
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder
|
||||
@@ -312,6 +334,10 @@ export const selectQueries = (state: RootState) => state.conversation.queries;
|
||||
|
||||
export const selectStatus = (state: RootState) => state.conversation.status;
|
||||
|
||||
export const selectAttachments = (state: RootState) => state.conversation.attachments;
|
||||
export const selectCompletedAttachments = (state: RootState) =>
|
||||
state.conversation.attachments.filter(att => att.status === 'completed');
|
||||
|
||||
export const {
|
||||
addQuery,
|
||||
updateQuery,
|
||||
@@ -323,5 +349,8 @@ export const {
|
||||
updateToolCalls,
|
||||
setConversation,
|
||||
setAttachments,
|
||||
addAttachment,
|
||||
updateAttachment,
|
||||
removeAttachment,
|
||||
} = conversationSlice.actions;
|
||||
export default conversationSlice.reducer;
|
||||
|
||||
10
frontend/src/utils/browserUtils.ts
Normal file
10
frontend/src/utils/browserUtils.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export function getOS() {
|
||||
const userAgent = window.navigator.userAgent;
|
||||
if (userAgent.indexOf('Mac') !== -1) return 'mac';
|
||||
if (userAgent.indexOf('Win') !== -1) return 'win';
|
||||
return 'linux';
|
||||
}
|
||||
|
||||
export function isTouchDevice() {
|
||||
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
}
|
||||
Reference in New Issue
Block a user