mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
Merge branch 'main' into feat/agent-menu
This commit is contained in:
20
README.md
20
README.md
@@ -49,11 +49,11 @@
|
||||
- [x] Manually updating chunks in the app UI (Feb 2025)
|
||||
- [x] Devcontainer for easy development (Feb 2025)
|
||||
- [x] ReACT agent (March 2025)
|
||||
- [ ] Anthropic Tool compatibility
|
||||
- [ ] New input box in the conversation menu
|
||||
- [ ] Add triggerable actions / tools (webhook)
|
||||
- [ ] Chatbots menu re-design to handle tools, agent types, and more (April 2025)
|
||||
- [ ] New input box in the conversation menu (April 2025)
|
||||
- [ ] Anthropic Tool compatibility (April 2025)
|
||||
- [ ] Add triggerable actions / tools (webhook) (April 2025)
|
||||
- [ ] Add OAuth 2.0 authentication for tools and sources
|
||||
- [ ] Chatbots menu re-design to handle tools, agent types, and more
|
||||
- [ ] Agent scheduling
|
||||
|
||||
You can find our full roadmap [here](https://github.com/orgs/arc53/projects/2). Please don't hesitate to contribute or create issues, it helps us improve DocsGPT!
|
||||
@@ -95,13 +95,15 @@ A more detailed [Quickstart](https://docs.docsgpt.cloud/quickstart) is available
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
This interactive script will guide you through setting up DocsGPT. It offers four options: using the public API, running locally, connecting to a local inference engine, or using a cloud API provider. The script will automatically configure your `.env` file and handle necessary downloads and installations based on your chosen option.
|
||||
|
||||
**For Windows:**
|
||||
|
||||
2. **Follow the Docker Deployment Guide:**
|
||||
2. **Run the PowerShell setup script:**
|
||||
|
||||
Please refer to the [Docker Deployment documentation](https://docs.docsgpt.cloud/Deploying/Docker-Deploying) for detailed step-by-step instructions on setting up DocsGPT using Docker.
|
||||
```powershell
|
||||
PowerShell -ExecutionPolicy Bypass -File .\setup.ps1
|
||||
```
|
||||
|
||||
Either script will guide you through setting up DocsGPT. Four options available: using the public API, running locally, connecting to a local inference engine, or using a cloud API provider. Scripts will automatically configure your `.env` file and handle necessary downloads and installations based on your chosen option.
|
||||
|
||||
**Navigate to http://localhost:5173/**
|
||||
|
||||
@@ -110,7 +112,7 @@ To stop DocsGPT, open a terminal in the `DocsGPT` directory and run:
|
||||
```bash
|
||||
docker compose -f deployment/docker-compose.yaml down
|
||||
```
|
||||
(or use the specific `docker compose down` command shown after running `setup.sh`).
|
||||
(or use the specific `docker compose down` command shown after running the setup script).
|
||||
|
||||
> [!Note]
|
||||
> For development environment setup instructions, please refer to the [Development Environment Guide](https://docs.docsgpt.cloud/Deploying/Development-Environment).
|
||||
|
||||
@@ -48,10 +48,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
|
||||
|
||||
@@ -657,6 +657,7 @@ class Answer(Resource):
|
||||
source_log_docs = []
|
||||
tool_calls = []
|
||||
stream_ended = False
|
||||
thought = ""
|
||||
|
||||
for line in complete_stream(
|
||||
question=question,
|
||||
@@ -679,6 +680,8 @@ class Answer(Resource):
|
||||
source_log_docs = event["source"]
|
||||
elif event["type"] == "tool_calls":
|
||||
tool_calls = event["tool_calls"]
|
||||
elif event["type"] == "thought":
|
||||
thought = event["thought"]
|
||||
elif event["type"] == "error":
|
||||
logger.error(f"Error from stream: {event['error']}")
|
||||
return bad_request(500, event["error"])
|
||||
@@ -710,6 +713,7 @@ class Answer(Resource):
|
||||
conversation_id,
|
||||
question,
|
||||
response_full,
|
||||
thought,
|
||||
source_log_docs,
|
||||
tool_calls,
|
||||
llm,
|
||||
@@ -876,14 +880,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}")
|
||||
|
||||
|
||||
@@ -2792,25 +2792,25 @@ 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
|
||||
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):
|
||||
|
||||
|
||||
@@ -73,7 +73,13 @@ class PandasCSVParser(BaseParser):
|
||||
for more information.
|
||||
Set to empty dict by default, this means pandas will try to figure
|
||||
out the separators, table head, etc. on its own.
|
||||
|
||||
|
||||
header_period (int): Controls how headers are included in output:
|
||||
- 0: Headers only at the beginning
|
||||
- 1: Headers in every row
|
||||
- N > 1: Headers every N rows
|
||||
|
||||
header_prefix (str): Prefix for header rows. Default is "HEADERS: ".
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -83,6 +89,8 @@ class PandasCSVParser(BaseParser):
|
||||
col_joiner: str = ", ",
|
||||
row_joiner: str = "\n",
|
||||
pandas_config: dict = {},
|
||||
header_period: int = 20,
|
||||
header_prefix: str = "HEADERS: ",
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
"""Init params."""
|
||||
@@ -91,6 +99,8 @@ class PandasCSVParser(BaseParser):
|
||||
self._col_joiner = col_joiner
|
||||
self._row_joiner = row_joiner
|
||||
self._pandas_config = pandas_config
|
||||
self._header_period = header_period
|
||||
self._header_prefix = header_prefix
|
||||
|
||||
def _init_parser(self) -> Dict:
|
||||
"""Init parser."""
|
||||
@@ -104,15 +114,26 @@ class PandasCSVParser(BaseParser):
|
||||
raise ValueError("pandas module is required to read CSV files.")
|
||||
|
||||
df = pd.read_csv(file, **self._pandas_config)
|
||||
headers = df.columns.tolist()
|
||||
header_row = f"{self._header_prefix}{self._col_joiner.join(headers)}"
|
||||
|
||||
text_list = df.apply(
|
||||
lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1
|
||||
).tolist()
|
||||
if not self._concat_rows:
|
||||
return df.apply(
|
||||
lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1
|
||||
).tolist()
|
||||
|
||||
text_list = []
|
||||
if self._header_period != 1:
|
||||
text_list.append(header_row)
|
||||
|
||||
for i, row in df.iterrows():
|
||||
if (self._header_period > 1 and i > 0 and i % self._header_period == 0):
|
||||
text_list.append(header_row)
|
||||
text_list.append(self._col_joiner.join(row.astype(str).tolist()))
|
||||
if self._header_period == 1 and i < len(df) - 1:
|
||||
text_list.append(header_row)
|
||||
|
||||
if self._concat_rows:
|
||||
return (self._row_joiner).join(text_list)
|
||||
else:
|
||||
return text_list
|
||||
return self._row_joiner.join(text_list)
|
||||
|
||||
|
||||
class ExcelParser(BaseParser):
|
||||
@@ -138,7 +159,13 @@ class ExcelParser(BaseParser):
|
||||
for more information.
|
||||
Set to empty dict by default, this means pandas will try to figure
|
||||
out the table structure on its own.
|
||||
|
||||
|
||||
header_period (int): Controls how headers are included in output:
|
||||
- 0: Headers only at the beginning (default)
|
||||
- 1: Headers in every row
|
||||
- N > 1: Headers every N rows
|
||||
|
||||
header_prefix (str): Prefix for header rows. Default is "HEADERS: ".
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -148,6 +175,8 @@ class ExcelParser(BaseParser):
|
||||
col_joiner: str = ", ",
|
||||
row_joiner: str = "\n",
|
||||
pandas_config: dict = {},
|
||||
header_period: int = 20,
|
||||
header_prefix: str = "HEADERS: ",
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
"""Init params."""
|
||||
@@ -156,6 +185,8 @@ class ExcelParser(BaseParser):
|
||||
self._col_joiner = col_joiner
|
||||
self._row_joiner = row_joiner
|
||||
self._pandas_config = pandas_config
|
||||
self._header_period = header_period
|
||||
self._header_prefix = header_prefix
|
||||
|
||||
def _init_parser(self) -> Dict:
|
||||
"""Init parser."""
|
||||
@@ -169,12 +200,22 @@ class ExcelParser(BaseParser):
|
||||
raise ValueError("pandas module is required to read Excel files.")
|
||||
|
||||
df = pd.read_excel(file, **self._pandas_config)
|
||||
headers = df.columns.tolist()
|
||||
header_row = f"{self._header_prefix}{self._col_joiner.join(headers)}"
|
||||
|
||||
if not self._concat_rows:
|
||||
return df.apply(
|
||||
lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1
|
||||
).tolist()
|
||||
|
||||
text_list = []
|
||||
if self._header_period != 1:
|
||||
text_list.append(header_row)
|
||||
|
||||
text_list = df.apply(
|
||||
lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1
|
||||
).tolist()
|
||||
|
||||
if self._concat_rows:
|
||||
return (self._row_joiner).join(text_list)
|
||||
else:
|
||||
return text_list
|
||||
for i, row in df.iterrows():
|
||||
if (self._header_period > 1 and i > 0 and i % self._header_period == 0):
|
||||
text_list.append(header_row)
|
||||
text_list.append(self._col_joiner.join(row.astype(str).tolist()))
|
||||
if self._header_period == 1 and i < len(df) - 1:
|
||||
text_list.append(header_row)
|
||||
return self._row_joiner.join(text_list)
|
||||
@@ -41,7 +41,7 @@ lxml==5.3.1
|
||||
markupsafe==3.0.2
|
||||
marshmallow==3.26.1
|
||||
mpmath==1.3.0
|
||||
multidict==6.1.0
|
||||
multidict==6.3.2
|
||||
mypy-extensions==1.0.0
|
||||
networkx==3.4.2
|
||||
numpy==2.2.1
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -73,9 +73,44 @@ The easiest way to launch DocsGPT is using the provided `setup.sh` script. This
|
||||
|
||||
## Launching DocsGPT (Windows)
|
||||
|
||||
For Windows users, we recommend following the Docker deployment guide for detailed instructions. Please refer to the [Docker Deployment documentation](/Deploying/Docker-Deploying) for step-by-step instructions on setting up DocsGPT on Windows using Docker.
|
||||
For Windows users, we provide a PowerShell script that offers the same functionality as the macOS/Linux setup script.
|
||||
|
||||
**Important for Windows:** Ensure Docker Desktop is installed and running correctly on your Windows system before proceeding.
|
||||
**Steps:**
|
||||
|
||||
1. **Download the DocsGPT Repository:**
|
||||
|
||||
First, you need to download the DocsGPT repository to your local machine. You can do this using Git:
|
||||
|
||||
```powershell
|
||||
git clone https://github.com/arc53/DocsGPT.git
|
||||
cd DocsGPT
|
||||
```
|
||||
|
||||
2. **Run the `setup.ps1` script:**
|
||||
|
||||
Execute the PowerShell setup script:
|
||||
|
||||
```powershell
|
||||
PowerShell -ExecutionPolicy Bypass -File .\setup.ps1
|
||||
```
|
||||
|
||||
3. **Follow the interactive setup:**
|
||||
|
||||
Just like the Linux/macOS script, the PowerShell script will guide you through setting DocsGPT.
|
||||
The script will handle environment configuration and start DocsGPT based on your selections.
|
||||
|
||||
4. **Access DocsGPT in your browser:**
|
||||
|
||||
Once the setup is complete and Docker containers are running, navigate to [http://localhost:5173/](http://localhost:5173/) in your web browser to access the DocsGPT web application.
|
||||
|
||||
5. **Stopping DocsGPT:**
|
||||
|
||||
To stop DocsGPT run the Docker Compose down command displayed at the end of the setup script's execution.
|
||||
|
||||
**Important for Windows:** Ensure Docker Desktop is installed and running correctly on your Windows system before proceeding. The script will attempt to start Docker if it's not running, but you may need to start it manually if there are issues.
|
||||
|
||||
**Alternative Method:**
|
||||
If you prefer a more manual approach, you can follow our [Docker Deployment documentation](/Deploying/Docker-Deploying) for detailed instructions on setting up DocsGPT on Windows using Docker commands directly.
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
|
||||
@@ -4,13 +4,20 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import endpoints from '../api/endpoints';
|
||||
import userService from '../api/services/userService';
|
||||
import AlertIcon from '../assets/alert.svg';
|
||||
import ClipIcon from '../assets/clip.svg';
|
||||
import ExitIcon from '../assets/exit.svg';
|
||||
import PaperPlane from '../assets/paper_plane.svg';
|
||||
import SourceIcon from '../assets/source.svg';
|
||||
import SpinnerDark from '../assets/spinner-dark.svg';
|
||||
import Spinner from '../assets/spinner.svg';
|
||||
import ToolIcon from '../assets/tool.svg';
|
||||
import { setAttachments } from '../conversation/conversationSlice';
|
||||
import {
|
||||
addAttachment,
|
||||
removeAttachment,
|
||||
selectAttachments,
|
||||
updateAttachment,
|
||||
} from '../conversation/conversationSlice';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import {
|
||||
@@ -18,6 +25,7 @@ import {
|
||||
selectToken,
|
||||
} from '../preferences/preferenceSlice';
|
||||
import Upload from '../upload/Upload';
|
||||
import { getOS, isTouchDevice } from '../utils/browserUtils';
|
||||
import SourcesPopup from './SourcesPopup';
|
||||
import ToolsPopup from './ToolsPopup';
|
||||
|
||||
@@ -56,13 +64,35 @@ export default function MessageInput({
|
||||
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;
|
||||
|
||||
@@ -73,23 +103,23 @@ 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 },
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -97,34 +127,35 @@ export default function MessageInput({
|
||||
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,
|
||||
),
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
taskId: newAttachment.taskId,
|
||||
updates: { status: 'failed' },
|
||||
}),
|
||||
);
|
||||
console.error('Error uploading file:', xhr.responseText);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
setUploads((prev) =>
|
||||
prev.map((upload, index) =>
|
||||
index === uploadIndex ? { ...upload, status: 'failed' } : upload,
|
||||
),
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
taskId: newAttachment.taskId,
|
||||
updates: { status: 'failed' },
|
||||
}),
|
||||
);
|
||||
console.error('Network error during file upload');
|
||||
};
|
||||
|
||||
xhr.open('POST', `${apiHost}${endpoints.USER.STORE_ATTACHMENT}`);
|
||||
@@ -134,69 +165,63 @@ 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,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} 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 },
|
||||
}),
|
||||
);
|
||||
timeoutIds.push(timeoutId);
|
||||
}
|
||||
})
|
||||
.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) {
|
||||
@@ -230,42 +255,56 @@ 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="mx-2 flex w-full flex-col">
|
||||
<div className="relative flex w-full flex-col rounded-[23px] border border-dark-gray bg-lotion dark:border-grey dark:bg-transparent">
|
||||
<div className="flex flex-wrap gap-1.5 px-4 pb-0 pt-3 sm:gap-2 sm:px-6">
|
||||
{uploads.map((upload, index) => (
|
||||
{attachments.map((attachment, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center rounded-[32px] border border-[#AAAAAA] bg-white px-2 py-1 text-[12px] text-[#5D5D5D] dark:border-purple-taupe dark:bg-[#1F2028] dark:text-bright-gray sm:px-3 sm:py-1.5 sm:text-[14px]"
|
||||
className={`group relative flex items-center rounded-[32px] border border-[#AAAAAA] bg-white px-2 py-1 text-[12px] text-[#5D5D5D] dark:border-purple-taupe dark:bg-[#1F2028] dark:text-bright-gray sm:px-3 sm:py-1.5 sm:text-[14px] ${
|
||||
attachment.status !== 'completed' ? 'opacity-70' : 'opacity-100'
|
||||
}`}
|
||||
title={attachment.fileName}
|
||||
>
|
||||
<span className="max-w-[120px] truncate font-medium sm:max-w-[150px]">
|
||||
{upload.fileName}
|
||||
{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 rounded-full bg-white p-1 opacity-0 transition-opacity hover:bg-white/95 focus:opacity-100 group-hover:opacity-100 dark:bg-[#1F2028] dark:hover:bg-[#1F2028]/95"
|
||||
onClick={() => {
|
||||
if (attachment.id) {
|
||||
dispatch(removeAttachment(attachment.id));
|
||||
}
|
||||
}}
|
||||
aria-label="Remove attachment"
|
||||
>
|
||||
<img
|
||||
src={ExitIcon}
|
||||
alt="Remove"
|
||||
className="h-2.5 w-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 h-3.5 w-3.5"
|
||||
title="Upload failed"
|
||||
/>
|
||||
)}
|
||||
|
||||
{(upload.status === 'uploading' ||
|
||||
upload.status === 'processing') && (
|
||||
{(attachment.status === 'uploading' ||
|
||||
attachment.status === 'processing') && (
|
||||
<div className="relative ml-2 h-4 w-4">
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24">
|
||||
{/* Background circle */}
|
||||
<circle
|
||||
className="text-gray-200 dark:text-gray-700"
|
||||
cx="12"
|
||||
@@ -284,7 +323,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>
|
||||
@@ -317,19 +356,29 @@ export default function MessageInput({
|
||||
{showSourceButton && (
|
||||
<button
|
||||
ref={sourceButtonRef}
|
||||
className="xs:px-3 xs:py-1.5 xs:max-w-[150px] flex max-w-[130px] items-center rounded-[32px] border border-[#AAAAAA] px-2 py-1 transition-colors hover:bg-gray-100 dark:border-purple-taupe dark:hover:bg-[#2C2E3C]"
|
||||
className="xs:px-3 xs:py-1.5 flex max-w-[130px] items-center rounded-[32px] border border-[#AAAAAA] px-2 py-1 transition-colors hover:bg-gray-100 dark:border-purple-taupe dark:hover:bg-[#2C2E3C] sm:max-w-[150px]"
|
||||
onClick={() => setIsSourcesPopupOpen(!isSourcesPopupOpen)}
|
||||
title={
|
||||
selectedDocs
|
||||
? selectedDocs.name
|
||||
: t('conversation.sources.title')
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={SourceIcon}
|
||||
alt="Sources"
|
||||
className="mr-1 h-3.5 w-3.5 flex-shrink-0 sm:mr-1.5 sm:h-4 sm:w-4"
|
||||
className="mr-1 h-3.5 w-3.5 flex-shrink-0 sm:mr-1.5 sm:h-4"
|
||||
/>
|
||||
<span className="xs:text-[12px] overflow-hidden truncate text-[10px] font-medium text-[#5D5D5D] dark:text-bright-gray sm:text-[14px]">
|
||||
{selectedDocs
|
||||
? selectedDocs.name
|
||||
: t('conversation.sources.title')}
|
||||
</span>
|
||||
{!isTouch && (
|
||||
<span className="ml-1 hidden text-[10px] text-gray-500 dark:text-gray-400 sm:inline-block">
|
||||
{browserOS === 'mac' ? '(⌘K)' : '(ctrl+K)'}
|
||||
</span>
|
||||
)}
|
||||
</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={() => {
|
||||
|
||||
@@ -171,7 +171,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,13 @@ 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 +44,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[];
|
||||
const currentConversationId = state.conversation.conversationId;
|
||||
const conversationIdToSend = isPreview ? null : currentConversationId;
|
||||
const save_conversation = isPreview ? false : true;
|
||||
@@ -129,7 +137,7 @@ export const fetchAnswer = createAsyncThunk<
|
||||
},
|
||||
indx,
|
||||
state.preference.selectedAgent?.id,
|
||||
attachments,
|
||||
attachmentIds,
|
||||
save_conversation,
|
||||
);
|
||||
} else {
|
||||
@@ -144,7 +152,7 @@ export const fetchAnswer = createAsyncThunk<
|
||||
state.preference.chunks,
|
||||
state.preference.token_limit,
|
||||
state.preference.selectedAgent?.id,
|
||||
attachments,
|
||||
attachmentIds,
|
||||
save_conversation,
|
||||
);
|
||||
if (answer) {
|
||||
@@ -301,12 +309,34 @@ 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,
|
||||
);
|
||||
},
|
||||
resetConversation: (state) => {
|
||||
state.queries = initialState.queries;
|
||||
state.status = initialState.status;
|
||||
@@ -337,6 +367,11 @@ 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,
|
||||
@@ -348,6 +383,9 @@ export const {
|
||||
updateToolCalls,
|
||||
setConversation,
|
||||
setAttachments,
|
||||
addAttachment,
|
||||
updateAttachment,
|
||||
removeAttachment,
|
||||
resetConversation,
|
||||
} = 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;
|
||||
}
|
||||
782
setup.ps1
Normal file
782
setup.ps1
Normal file
@@ -0,0 +1,782 @@
|
||||
# DocsGPT Setup PowerShell Script for Windows
|
||||
# PowerShell -ExecutionPolicy Bypass -File .\setup.ps1
|
||||
|
||||
# Script execution policy - uncomment if needed
|
||||
# Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
|
||||
|
||||
# Set error action preference
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Get current script directory
|
||||
$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$COMPOSE_FILE = Join-Path -Path $SCRIPT_DIR -ChildPath "deployment\docker-compose.yaml"
|
||||
$ENV_FILE = Join-Path -Path $SCRIPT_DIR -ChildPath ".env"
|
||||
|
||||
# Function to write colored text
|
||||
function Write-ColorText {
|
||||
param (
|
||||
[Parameter(Mandatory=$true)][string]$Text,
|
||||
[Parameter()][string]$ForegroundColor = "White",
|
||||
[Parameter()][switch]$Bold
|
||||
)
|
||||
|
||||
$params = @{
|
||||
ForegroundColor = $ForegroundColor
|
||||
NoNewline = $false
|
||||
}
|
||||
|
||||
if ($Bold) {
|
||||
# PowerShell doesn't have bold
|
||||
Write-Host $Text @params
|
||||
} else {
|
||||
Write-Host $Text @params
|
||||
}
|
||||
}
|
||||
|
||||
# Animation function (Windows PowerShell version of animate_dino)
|
||||
function Animate-Dino {
|
||||
[Console]::CursorVisible = $false
|
||||
|
||||
# Clear screen
|
||||
Clear-Host
|
||||
|
||||
# Static DocsGPT text
|
||||
$static_text = @(
|
||||
" ____ ____ ____ _____ "
|
||||
" | _ \ ___ ___ ___ / ___| _ \_ _|"
|
||||
" | | | |/ _ \ / __/ __| | _| |_) || | "
|
||||
" | |_| | (_) | (__\__ \ |_| | __/ | | "
|
||||
" |____/ \___/ \___|___/\____|_| |_| "
|
||||
" "
|
||||
)
|
||||
|
||||
# Print static text
|
||||
foreach ($line in $static_text) {
|
||||
Write-Host $line
|
||||
}
|
||||
|
||||
# Dino ASCII art
|
||||
$dino_lines = @(
|
||||
" ######### "
|
||||
" ############# "
|
||||
" ##################"
|
||||
" ####################"
|
||||
" ######################"
|
||||
" ####################### ######"
|
||||
" ############################### "
|
||||
" ################################## "
|
||||
" ################ ############ "
|
||||
" ################## ########## "
|
||||
" ##################### ######## "
|
||||
" ###################### ###### ### "
|
||||
" ############ ########## #### ## "
|
||||
" ############# ######### ##### "
|
||||
" ############## ######### "
|
||||
" ############## ########## "
|
||||
"############ ####### "
|
||||
" ###### ###### #### "
|
||||
" ################ "
|
||||
" ################# "
|
||||
)
|
||||
|
||||
# Save cursor position
|
||||
$cursorPos = $Host.UI.RawUI.CursorPosition
|
||||
|
||||
# Build-up animation
|
||||
for ($i = 0; $i -lt $dino_lines.Count; $i++) {
|
||||
# Restore cursor position
|
||||
$Host.UI.RawUI.CursorPosition = $cursorPos
|
||||
|
||||
# Display lines up to current index
|
||||
for ($j = 0; $j -le $i; $j++) {
|
||||
Write-Host $dino_lines[$j]
|
||||
}
|
||||
|
||||
# Slow down animation
|
||||
Start-Sleep -Milliseconds 50
|
||||
}
|
||||
|
||||
# Pause at end of animation
|
||||
Start-Sleep -Milliseconds 500
|
||||
|
||||
# Clear the animation
|
||||
$Host.UI.RawUI.CursorPosition = $cursorPos
|
||||
|
||||
# Clear from cursor to end of screen
|
||||
for ($i = 0; $i -lt $dino_lines.Count; $i++) {
|
||||
Write-Host (" " * $dino_lines[0].Length)
|
||||
}
|
||||
|
||||
# Restore cursor position for next output
|
||||
$Host.UI.RawUI.CursorPosition = $cursorPos
|
||||
|
||||
# Show cursor again
|
||||
[Console]::CursorVisible = $true
|
||||
}
|
||||
|
||||
# Check and start Docker function
|
||||
function Check-AndStartDocker {
|
||||
# Check if Docker is running
|
||||
try {
|
||||
$dockerRunning = $false
|
||||
|
||||
# First try with 'docker info' which should work if Docker is fully operational
|
||||
try {
|
||||
$dockerInfo = docker info 2>&1
|
||||
# If we get here without an exception, Docker is running
|
||||
Write-ColorText "Docker is already running." -ForegroundColor "Green"
|
||||
return $true
|
||||
} catch {
|
||||
# Docker info command failed
|
||||
}
|
||||
|
||||
# Check if Docker process is running
|
||||
$dockerProcess = Get-Process "Docker Desktop" -ErrorAction SilentlyContinue
|
||||
if ($dockerProcess) {
|
||||
# Docker Desktop is running, but might not be fully initialized
|
||||
Write-ColorText "Docker Desktop is starting up. Waiting for it to be ready..." -ForegroundColor "Yellow"
|
||||
|
||||
# Wait for Docker to become operational
|
||||
$attempts = 0
|
||||
$maxAttempts = 30
|
||||
|
||||
while ($attempts -lt $maxAttempts) {
|
||||
try {
|
||||
$null = docker ps 2>&1
|
||||
Write-ColorText "Docker is now operational." -ForegroundColor "Green"
|
||||
return $true
|
||||
} catch {
|
||||
Write-Host "." -NoNewline
|
||||
Start-Sleep -Seconds 2
|
||||
$attempts++
|
||||
}
|
||||
}
|
||||
|
||||
Write-ColorText "`nDocker Desktop is running but not responding to commands. Please check Docker status." -ForegroundColor "Red"
|
||||
return $false
|
||||
}
|
||||
|
||||
# Docker is not running, attempt to start it
|
||||
Write-ColorText "Docker is not running. Attempting to start Docker Desktop..." -ForegroundColor "Yellow"
|
||||
|
||||
# Docker Desktop locations to check
|
||||
$dockerPaths = @(
|
||||
"${env:ProgramFiles}\Docker\Docker\Docker Desktop.exe",
|
||||
"${env:ProgramFiles(x86)}\Docker\Docker\Docker Desktop.exe",
|
||||
"$env:LOCALAPPDATA\Docker\Docker\Docker Desktop.exe"
|
||||
)
|
||||
|
||||
$dockerPath = $null
|
||||
foreach ($path in $dockerPaths) {
|
||||
if (Test-Path $path) {
|
||||
$dockerPath = $path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -eq $dockerPath) {
|
||||
Write-ColorText "Docker Desktop not found. Please install Docker Desktop or start it manually." -ForegroundColor "Red"
|
||||
return $false
|
||||
}
|
||||
|
||||
# Start Docker Desktop
|
||||
try {
|
||||
Start-Process $dockerPath
|
||||
Write-Host -NoNewline "Waiting for Docker to start"
|
||||
|
||||
# Wait for Docker to be ready
|
||||
$attempts = 0
|
||||
$maxAttempts = 60 # 60 x 2 seconds = maximum 2 minutes wait
|
||||
|
||||
while ($attempts -lt $maxAttempts) {
|
||||
try {
|
||||
$null = docker ps 2>&1
|
||||
Write-Host "`nDocker has started successfully!"
|
||||
return $true
|
||||
} catch {
|
||||
# Show waiting animation
|
||||
Write-Host -NoNewline "."
|
||||
Start-Sleep -Seconds 2
|
||||
$attempts++
|
||||
|
||||
if ($attempts % 3 -eq 0) {
|
||||
Write-Host "`r" -NoNewline
|
||||
Write-Host "Waiting for Docker to start " -NoNewline
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-ColorText "`nDocker did not start within the expected time. Please start Docker Desktop manually." -ForegroundColor "Red"
|
||||
return $false
|
||||
} catch {
|
||||
Write-ColorText "Failed to start Docker Desktop. Please start it manually." -ForegroundColor "Red"
|
||||
return $false
|
||||
}
|
||||
} catch {
|
||||
Write-ColorText "Error checking Docker status: $_" -ForegroundColor "Red"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# Function to prompt the user for the main menu choice
|
||||
function Prompt-MainMenu {
|
||||
Write-Host ""
|
||||
Write-ColorText "Welcome to DocsGPT Setup!" -ForegroundColor "White" -Bold
|
||||
Write-ColorText "How would you like to proceed?" -ForegroundColor "White"
|
||||
Write-ColorText "1) Use DocsGPT Public API Endpoint (simple and free)" -ForegroundColor "Yellow"
|
||||
Write-ColorText "2) Serve Local (with Ollama)" -ForegroundColor "Yellow"
|
||||
Write-ColorText "3) Connect Local Inference Engine" -ForegroundColor "Yellow"
|
||||
Write-ColorText "4) Connect Cloud API Provider" -ForegroundColor "Yellow"
|
||||
Write-Host ""
|
||||
$script:main_choice = Read-Host "Choose option (1-4)"
|
||||
}
|
||||
|
||||
# Function to prompt for Local Inference Engine options
|
||||
function Prompt-LocalInferenceEngineOptions {
|
||||
Clear-Host
|
||||
Write-Host ""
|
||||
Write-ColorText "Connect Local Inference Engine" -ForegroundColor "White" -Bold
|
||||
Write-ColorText "Choose your local inference engine:" -ForegroundColor "White"
|
||||
Write-ColorText "1) LLaMa.cpp" -ForegroundColor "Yellow"
|
||||
Write-ColorText "2) Ollama" -ForegroundColor "Yellow"
|
||||
Write-ColorText "3) Text Generation Inference (TGI)" -ForegroundColor "Yellow"
|
||||
Write-ColorText "4) SGLang" -ForegroundColor "Yellow"
|
||||
Write-ColorText "5) vLLM" -ForegroundColor "Yellow"
|
||||
Write-ColorText "6) Aphrodite" -ForegroundColor "Yellow"
|
||||
Write-ColorText "7) FriendliAI" -ForegroundColor "Yellow"
|
||||
Write-ColorText "8) LMDeploy" -ForegroundColor "Yellow"
|
||||
Write-ColorText "b) Back to Main Menu" -ForegroundColor "Yellow"
|
||||
Write-Host ""
|
||||
$script:engine_choice = Read-Host "Choose option (1-8, or b)"
|
||||
}
|
||||
|
||||
# Function to prompt for Cloud API Provider options
|
||||
function Prompt-CloudAPIProviderOptions {
|
||||
Clear-Host
|
||||
Write-Host ""
|
||||
Write-ColorText "Connect Cloud API Provider" -ForegroundColor "White" -Bold
|
||||
Write-ColorText "Choose your Cloud API Provider:" -ForegroundColor "White"
|
||||
Write-ColorText "1) OpenAI" -ForegroundColor "Yellow"
|
||||
Write-ColorText "2) Google (Vertex AI, Gemini)" -ForegroundColor "Yellow"
|
||||
Write-ColorText "3) Anthropic (Claude)" -ForegroundColor "Yellow"
|
||||
Write-ColorText "4) Groq" -ForegroundColor "Yellow"
|
||||
Write-ColorText "5) HuggingFace Inference API" -ForegroundColor "Yellow"
|
||||
Write-ColorText "6) Azure OpenAI" -ForegroundColor "Yellow"
|
||||
Write-ColorText "7) Novita" -ForegroundColor "Yellow"
|
||||
Write-ColorText "b) Back to Main Menu" -ForegroundColor "Yellow"
|
||||
Write-Host ""
|
||||
$script:provider_choice = Read-Host "Choose option (1-7, or b)"
|
||||
}
|
||||
|
||||
# Function to prompt for Ollama CPU/GPU options
|
||||
function Prompt-OllamaOptions {
|
||||
Clear-Host
|
||||
Write-Host ""
|
||||
Write-ColorText "Serve Local with Ollama" -ForegroundColor "White" -Bold
|
||||
Write-ColorText "Choose how to serve Ollama:" -ForegroundColor "White"
|
||||
Write-ColorText "1) CPU" -ForegroundColor "Yellow"
|
||||
Write-ColorText "2) GPU" -ForegroundColor "Yellow"
|
||||
Write-ColorText "b) Back to Main Menu" -ForegroundColor "Yellow"
|
||||
Write-Host ""
|
||||
$script:ollama_choice = Read-Host "Choose option (1-2, or b)"
|
||||
}
|
||||
|
||||
# 1) Use DocsGPT Public API Endpoint (simple and free)
|
||||
function Use-DocsPublicAPIEndpoint {
|
||||
Write-Host ""
|
||||
Write-ColorText "Setting up DocsGPT Public API Endpoint..." -ForegroundColor "White"
|
||||
|
||||
# Create .env file
|
||||
"LLM_NAME=docsgpt" | Out-File -FilePath $ENV_FILE -Encoding utf8 -Force
|
||||
"VITE_API_STREAMING=true" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
|
||||
Write-ColorText ".env file configured for DocsGPT Public API." -ForegroundColor "Green"
|
||||
|
||||
# Start Docker if needed
|
||||
$dockerRunning = Check-AndStartDocker
|
||||
if (-not $dockerRunning) {
|
||||
Write-ColorText "Docker is required but could not be started. Please start Docker Desktop manually and try again." -ForegroundColor "Red"
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-ColorText "Starting Docker Compose..." -ForegroundColor "White"
|
||||
|
||||
# Run Docker compose commands
|
||||
try {
|
||||
& docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" build
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker compose build failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
& docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker compose up failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-ColorText "DocsGPT is now running on http://localhost:5173" -ForegroundColor "Green"
|
||||
Write-ColorText "You can stop the application by running: docker compose -f `"$COMPOSE_FILE`" down" -ForegroundColor "Yellow"
|
||||
}
|
||||
catch {
|
||||
Write-Host ""
|
||||
Write-ColorText "Error starting Docker Compose: $_" -ForegroundColor "Red"
|
||||
Write-ColorText "Please ensure Docker Compose is installed and in your PATH." -ForegroundColor "Red"
|
||||
Write-ColorText "Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/" -ForegroundColor "Red"
|
||||
exit 1 # Exit script with error
|
||||
}
|
||||
}
|
||||
|
||||
# 2) Serve Local (with Ollama)
|
||||
function Serve-LocalOllama {
|
||||
$script:model_name = ""
|
||||
$default_model = "llama3.2:1b"
|
||||
$docker_compose_file_suffix = ""
|
||||
|
||||
function Get-ModelNameOllama {
|
||||
$model_name_input = Read-Host "Enter Ollama Model Name (press Enter for default: $default_model (1.3GB))"
|
||||
if ([string]::IsNullOrEmpty($model_name_input)) {
|
||||
$script:model_name = $default_model
|
||||
} else {
|
||||
$script:model_name = $model_name_input
|
||||
}
|
||||
}
|
||||
|
||||
while ($true) {
|
||||
Clear-Host
|
||||
Prompt-OllamaOptions
|
||||
|
||||
switch ($ollama_choice) {
|
||||
"1" { # CPU
|
||||
$docker_compose_file_suffix = "cpu"
|
||||
Get-ModelNameOllama
|
||||
break
|
||||
}
|
||||
"2" { # GPU
|
||||
Write-Host ""
|
||||
Write-ColorText "For this option to work correctly you need to have a supported GPU and configure Docker to utilize it." -ForegroundColor "Yellow"
|
||||
Write-ColorText "Refer to: https://hub.docker.com/r/ollama/ollama for more information." -ForegroundColor "Yellow"
|
||||
$confirm_gpu = Read-Host "Continue with GPU setup? (y/b)"
|
||||
|
||||
if ($confirm_gpu -eq "y" -or $confirm_gpu -eq "Y") {
|
||||
$docker_compose_file_suffix = "gpu"
|
||||
Get-ModelNameOllama
|
||||
break
|
||||
}
|
||||
elseif ($confirm_gpu -eq "b" -or $confirm_gpu -eq "B") {
|
||||
Clear-Host
|
||||
return
|
||||
}
|
||||
else {
|
||||
Write-Host ""
|
||||
Write-ColorText "Invalid choice. Please choose y or b." -ForegroundColor "Red"
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
}
|
||||
"b" { Clear-Host; return }
|
||||
"B" { Clear-Host; return }
|
||||
default {
|
||||
Write-Host ""
|
||||
Write-ColorText "Invalid choice. Please choose 1-2, or b." -ForegroundColor "Red"
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrEmpty($docker_compose_file_suffix)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-ColorText "Configuring for Ollama ($($docker_compose_file_suffix.ToUpper()))..." -ForegroundColor "White"
|
||||
|
||||
# Create .env file
|
||||
"API_KEY=xxxx" | Out-File -FilePath $ENV_FILE -Encoding utf8 -Force
|
||||
"LLM_NAME=openai" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"MODEL_NAME=$model_name" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"VITE_API_STREAMING=true" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"OPENAI_BASE_URL=http://host.docker.internal:11434/v1" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
|
||||
Write-ColorText ".env file configured for Ollama ($($docker_compose_file_suffix.ToUpper()))." -ForegroundColor "Green"
|
||||
Write-ColorText "Note: MODEL_NAME is set to '$model_name'. You can change it later in the .env file." -ForegroundColor "Yellow"
|
||||
|
||||
# Start Docker if needed
|
||||
$dockerRunning = Check-AndStartDocker
|
||||
if (-not $dockerRunning) {
|
||||
Write-ColorText "Docker is required but could not be started. Please start Docker Desktop manually and try again." -ForegroundColor "Red"
|
||||
return
|
||||
}
|
||||
|
||||
# Setup compose file paths
|
||||
$optional_compose = Join-Path -Path (Split-Path -Parent $COMPOSE_FILE) -ChildPath "optional\docker-compose.optional.ollama-$docker_compose_file_suffix.yaml"
|
||||
|
||||
try {
|
||||
Write-Host ""
|
||||
Write-ColorText "Starting Docker Compose with Ollama ($docker_compose_file_suffix)..." -ForegroundColor "White"
|
||||
|
||||
# Build the containers
|
||||
& docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" -f "$optional_compose" build
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker compose build failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
# Start the containers
|
||||
& docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" -f "$optional_compose" up -d
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker compose up failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
# Wait for Ollama container to be ready
|
||||
Write-ColorText "Waiting for Ollama container to be ready..." -ForegroundColor "White"
|
||||
$ollamaReady = $false
|
||||
$maxAttempts = 30 # Maximum number of attempts (30 x 5 seconds = 2.5 minutes)
|
||||
$attempts = 0
|
||||
|
||||
while (-not $ollamaReady -and $attempts -lt $maxAttempts) {
|
||||
$containerStatus = & docker compose -f "$COMPOSE_FILE" -f "$optional_compose" ps --services --filter "status=running" --format "{{.Service}}"
|
||||
|
||||
if ($containerStatus -like "*ollama*") {
|
||||
$ollamaReady = $true
|
||||
Write-ColorText "Ollama container is running." -ForegroundColor "Green"
|
||||
} else {
|
||||
Write-Host "Ollama container not yet ready, waiting... (Attempt $($attempts+1)/$maxAttempts)"
|
||||
Start-Sleep -Seconds 5
|
||||
$attempts++
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $ollamaReady) {
|
||||
Write-ColorText "Ollama container did not start within the expected time. Please check Docker logs for errors." -ForegroundColor "Red"
|
||||
return
|
||||
}
|
||||
|
||||
# Pull the Ollama model
|
||||
Write-ColorText "Pulling $model_name model for Ollama..." -ForegroundColor "White"
|
||||
& docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" -f "$optional_compose" exec -it ollama ollama pull "$model_name"
|
||||
|
||||
Write-Host ""
|
||||
Write-ColorText "DocsGPT is now running with Ollama ($docker_compose_file_suffix) on http://localhost:5173" -ForegroundColor "Green"
|
||||
Write-ColorText "You can stop the application by running: docker compose -f `"$COMPOSE_FILE`" -f `"$optional_compose`" down" -ForegroundColor "Yellow"
|
||||
}
|
||||
catch {
|
||||
Write-Host ""
|
||||
Write-ColorText "Error running Docker Compose: $_" -ForegroundColor "Red"
|
||||
Write-ColorText "Please ensure Docker Compose is installed and in your PATH." -ForegroundColor "Red"
|
||||
Write-ColorText "Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/" -ForegroundColor "Red"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# 3) Connect Local Inference Engine
|
||||
function Connect-LocalInferenceEngine {
|
||||
$script:engine_name = ""
|
||||
$script:openai_base_url = ""
|
||||
$script:model_name = ""
|
||||
|
||||
function Get-ModelName {
|
||||
$model_name_input = Read-Host "Enter Model Name (press Enter for None)"
|
||||
if ([string]::IsNullOrEmpty($model_name_input)) {
|
||||
$script:model_name = "None"
|
||||
} else {
|
||||
$script:model_name = $model_name_input
|
||||
}
|
||||
}
|
||||
|
||||
while ($true) {
|
||||
Clear-Host
|
||||
Prompt-LocalInferenceEngineOptions
|
||||
|
||||
switch ($engine_choice) {
|
||||
"1" { # LLaMa.cpp
|
||||
$script:engine_name = "LLaMa.cpp"
|
||||
$script:openai_base_url = "http://localhost:8000/v1"
|
||||
Get-ModelName
|
||||
break
|
||||
}
|
||||
"2" { # Ollama
|
||||
$script:engine_name = "Ollama"
|
||||
$script:openai_base_url = "http://localhost:11434/v1"
|
||||
Get-ModelName
|
||||
break
|
||||
}
|
||||
"3" { # TGI
|
||||
$script:engine_name = "TGI"
|
||||
$script:openai_base_url = "http://localhost:8080/v1"
|
||||
Get-ModelName
|
||||
break
|
||||
}
|
||||
"4" { # SGLang
|
||||
$script:engine_name = "SGLang"
|
||||
$script:openai_base_url = "http://localhost:30000/v1"
|
||||
Get-ModelName
|
||||
break
|
||||
}
|
||||
"5" { # vLLM
|
||||
$script:engine_name = "vLLM"
|
||||
$script:openai_base_url = "http://localhost:8000/v1"
|
||||
Get-ModelName
|
||||
break
|
||||
}
|
||||
"6" { # Aphrodite
|
||||
$script:engine_name = "Aphrodite"
|
||||
$script:openai_base_url = "http://localhost:2242/v1"
|
||||
Get-ModelName
|
||||
break
|
||||
}
|
||||
"7" { # FriendliAI
|
||||
$script:engine_name = "FriendliAI"
|
||||
$script:openai_base_url = "http://localhost:8997/v1"
|
||||
Get-ModelName
|
||||
break
|
||||
}
|
||||
"8" { # LMDeploy
|
||||
$script:engine_name = "LMDeploy"
|
||||
$script:openai_base_url = "http://localhost:23333/v1"
|
||||
Get-ModelName
|
||||
break
|
||||
}
|
||||
"b" { Clear-Host; return }
|
||||
"B" { Clear-Host; return }
|
||||
default {
|
||||
Write-Host ""
|
||||
Write-ColorText "Invalid choice. Please choose 1-8, or b." -ForegroundColor "Red"
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrEmpty($script:engine_name)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-ColorText "Configuring for Local Inference Engine: $engine_name..." -ForegroundColor "White"
|
||||
|
||||
# Create .env file
|
||||
"API_KEY=None" | Out-File -FilePath $ENV_FILE -Encoding utf8 -Force
|
||||
"LLM_NAME=openai" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"MODEL_NAME=$model_name" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"VITE_API_STREAMING=true" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"OPENAI_BASE_URL=$openai_base_url" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
|
||||
Write-ColorText ".env file configured for $engine_name with OpenAI API format." -ForegroundColor "Green"
|
||||
Write-ColorText "Note: MODEL_NAME is set to '$model_name'. You can change it later in the .env file." -ForegroundColor "Yellow"
|
||||
|
||||
# Start Docker if needed
|
||||
$dockerRunning = Check-AndStartDocker
|
||||
if (-not $dockerRunning) {
|
||||
Write-ColorText "Docker is required but could not be started. Please start Docker Desktop manually and try again." -ForegroundColor "Red"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Host ""
|
||||
Write-ColorText "Starting Docker Compose..." -ForegroundColor "White"
|
||||
|
||||
# Build the containers
|
||||
& docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" build
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker compose build failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
# Start the containers
|
||||
& docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker compose up failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-ColorText "DocsGPT is now configured to connect to $engine_name at $openai_base_url" -ForegroundColor "Green"
|
||||
Write-ColorText "Ensure your $engine_name inference server is running at that address" -ForegroundColor "Yellow"
|
||||
Write-Host ""
|
||||
Write-ColorText "DocsGPT is running at http://localhost:5173" -ForegroundColor "Green"
|
||||
Write-ColorText "You can stop the application by running: docker compose -f `"$COMPOSE_FILE`" down" -ForegroundColor "Yellow"
|
||||
}
|
||||
catch {
|
||||
Write-Host ""
|
||||
Write-ColorText "Error running Docker Compose: $_" -ForegroundColor "Red"
|
||||
Write-ColorText "Please ensure Docker Compose is installed and in your PATH." -ForegroundColor "Red"
|
||||
Write-ColorText "Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/" -ForegroundColor "Red"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# 4) Connect Cloud API Provider
|
||||
function Connect-CloudAPIProvider {
|
||||
$script:provider_name = ""
|
||||
$script:llm_name = ""
|
||||
$script:model_name = ""
|
||||
$script:api_key = ""
|
||||
|
||||
function Get-APIKey {
|
||||
Write-ColorText "Your API key will be stored locally in the .env file and will not be sent anywhere else" -ForegroundColor "Yellow"
|
||||
$script:api_key = Read-Host "Please enter your API key"
|
||||
}
|
||||
|
||||
while ($true) {
|
||||
Clear-Host
|
||||
Prompt-CloudAPIProviderOptions
|
||||
|
||||
switch ($provider_choice) {
|
||||
"1" { # OpenAI
|
||||
$script:provider_name = "OpenAI"
|
||||
$script:llm_name = "openai"
|
||||
$script:model_name = "gpt-4o"
|
||||
Get-APIKey
|
||||
break
|
||||
}
|
||||
"2" { # Google
|
||||
$script:provider_name = "Google (Vertex AI, Gemini)"
|
||||
$script:llm_name = "google"
|
||||
$script:model_name = "gemini-2.0-flash"
|
||||
Get-APIKey
|
||||
break
|
||||
}
|
||||
"3" { # Anthropic
|
||||
$script:provider_name = "Anthropic (Claude)"
|
||||
$script:llm_name = "anthropic"
|
||||
$script:model_name = "claude-3-5-sonnet-latest"
|
||||
Get-APIKey
|
||||
break
|
||||
}
|
||||
"4" { # Groq
|
||||
$script:provider_name = "Groq"
|
||||
$script:llm_name = "groq"
|
||||
$script:model_name = "llama-3.1-8b-instant"
|
||||
Get-APIKey
|
||||
break
|
||||
}
|
||||
"5" { # HuggingFace Inference API
|
||||
$script:provider_name = "HuggingFace Inference API"
|
||||
$script:llm_name = "huggingface"
|
||||
$script:model_name = "meta-llama/Llama-3.1-8B-Instruct"
|
||||
Get-APIKey
|
||||
break
|
||||
}
|
||||
"6" { # Azure OpenAI
|
||||
$script:provider_name = "Azure OpenAI"
|
||||
$script:llm_name = "azure_openai"
|
||||
$script:model_name = "gpt-4o"
|
||||
Get-APIKey
|
||||
break
|
||||
}
|
||||
"7" { # Novita
|
||||
$script:provider_name = "Novita"
|
||||
$script:llm_name = "novita"
|
||||
$script:model_name = "deepseek/deepseek-r1"
|
||||
Get-APIKey
|
||||
break
|
||||
}
|
||||
"b" { Clear-Host; return }
|
||||
"B" { Clear-Host; return }
|
||||
default {
|
||||
Write-Host ""
|
||||
Write-ColorText "Invalid choice. Please choose 1-7, or b." -ForegroundColor "Red"
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrEmpty($script:provider_name)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-ColorText "Configuring for Cloud API Provider: $provider_name..." -ForegroundColor "White"
|
||||
|
||||
# Create .env file
|
||||
"API_KEY=$api_key" | Out-File -FilePath $ENV_FILE -Encoding utf8 -Force
|
||||
"LLM_NAME=$llm_name" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"MODEL_NAME=$model_name" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
"VITE_API_STREAMING=true" | Add-Content -Path $ENV_FILE -Encoding utf8
|
||||
|
||||
Write-ColorText ".env file configured for $provider_name." -ForegroundColor "Green"
|
||||
|
||||
# Start Docker if needed
|
||||
$dockerRunning = Check-AndStartDocker
|
||||
if (-not $dockerRunning) {
|
||||
Write-ColorText "Docker is required but could not be started. Please start Docker Desktop manually and try again." -ForegroundColor "Red"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Host ""
|
||||
Write-ColorText "Starting Docker Compose..." -ForegroundColor "White"
|
||||
|
||||
# Run Docker compose commands
|
||||
& docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d --build
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker compose build or up failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-ColorText "DocsGPT is now configured to use $provider_name on http://localhost:5173" -ForegroundColor "Green"
|
||||
Write-ColorText "You can stop the application by running: docker compose -f `"$COMPOSE_FILE`" down" -ForegroundColor "Yellow"
|
||||
}
|
||||
catch {
|
||||
Write-Host ""
|
||||
Write-ColorText "Error running Docker Compose: $_" -ForegroundColor "Red"
|
||||
Write-ColorText "Please ensure Docker Compose is installed and in your PATH." -ForegroundColor "Red"
|
||||
Write-ColorText "Refer to Docker documentation for installation instructions: https://docs.docker.com/compose/install/" -ForegroundColor "Red"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Main script execution
|
||||
Animate-Dino
|
||||
|
||||
while ($true) {
|
||||
Clear-Host
|
||||
Prompt-MainMenu
|
||||
|
||||
$exitLoop = $false # Add this flag
|
||||
|
||||
switch ($main_choice) {
|
||||
"1" {
|
||||
Use-DocsPublicAPIEndpoint
|
||||
$exitLoop = $true # Set flag to true on completion
|
||||
break
|
||||
}
|
||||
"2" {
|
||||
Serve-LocalOllama
|
||||
# Only exit the loop if user didn't press "b" to go back
|
||||
if ($ollama_choice -ne "b" -and $ollama_choice -ne "B") {
|
||||
$exitLoop = $true
|
||||
}
|
||||
break
|
||||
}
|
||||
"3" {
|
||||
Connect-LocalInferenceEngine
|
||||
# Only exit the loop if user didn't press "b" to go back
|
||||
if ($engine_choice -ne "b" -and $engine_choice -ne "B") {
|
||||
$exitLoop = $true
|
||||
}
|
||||
break
|
||||
}
|
||||
"4" {
|
||||
Connect-CloudAPIProvider
|
||||
# Only exit the loop if user didn't press "b" to go back
|
||||
if ($provider_choice -ne "b" -and $provider_choice -ne "B") {
|
||||
$exitLoop = $true
|
||||
}
|
||||
break
|
||||
}
|
||||
default {
|
||||
Write-Host ""
|
||||
Write-ColorText "Invalid choice. Please choose 1-4." -ForegroundColor "Red"
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
}
|
||||
|
||||
# Only break out of the loop if a function completed successfully
|
||||
if ($exitLoop) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-ColorText "DocsGPT Setup Complete." -ForegroundColor "Green"
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user