Compare commits
67 Commits
dependabot
...
0.14.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c78518baf0 | ||
|
|
556d7e0497 | ||
|
|
2d27936dab | ||
|
|
63f6127049 | ||
|
|
f34e00c986 | ||
|
|
55f60a9fe1 | ||
|
|
7da3618e0c | ||
|
|
56bfa98633 | ||
|
|
96f6188722 | ||
|
|
6c3a79802e | ||
|
|
c35c5e0793 | ||
|
|
fc01b90007 | ||
|
|
e35f1d70e4 | ||
|
|
cab1f3787a | ||
|
|
bb42f4cbc1 | ||
|
|
98dc418a51 | ||
|
|
322b4eb18c | ||
|
|
7f1cc30ed8 | ||
|
|
7b45a6b956 | ||
|
|
e36769e70f | ||
|
|
bd4a4cc4af | ||
|
|
8343fe63cb | ||
|
|
7d89fb8461 | ||
|
|
098955d230 | ||
|
|
d254d14928 | ||
|
|
0a3e8ca535 | ||
|
|
b8a10e0962 | ||
|
|
0aceda96e4 | ||
|
|
44b6ec25a2 | ||
|
|
1b84d1fa9d | ||
|
|
78d5ed2ed2 | ||
|
|
142477ab9b | ||
|
|
b414f79bc5 | ||
|
|
6e08fe21d0 | ||
|
|
9b839655a7 | ||
|
|
3353c0ee1d | ||
|
|
aaecf52c99 | ||
|
|
8b3e960be0 | ||
|
|
3351f71813 | ||
|
|
7490256303 | ||
|
|
041d600e45 | ||
|
|
b4e2588a24 | ||
|
|
68dc14c5a1 | ||
|
|
ef35864e16 | ||
|
|
c0d385b983 | ||
|
|
b2df431fa4 | ||
|
|
69a4bd415a | ||
|
|
4862548e65 | ||
|
|
50248cc9ea | ||
|
|
430822bae3 | ||
|
|
dd9d18208d | ||
|
|
e5b1a71659 | ||
|
|
35f4b13237 | ||
|
|
5f5c31cd5b | ||
|
|
e9530d5ec5 | ||
|
|
143f4aa886 | ||
|
|
ece5c8bb31 | ||
|
|
3bae30c70c | ||
|
|
12b18c6bd1 | ||
|
|
787d9e3bf5 | ||
|
|
f325b54895 | ||
|
|
c5616705b0 | ||
|
|
e90fe117ec | ||
|
|
7cab5b3b09 | ||
|
|
9f911cb5cb | ||
|
|
3da7cba06c | ||
|
|
92c3c707e1 |
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
@@ -2,16 +2,18 @@ import uuid
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Dict, Generator, List, Optional
|
from typing import Dict, Generator, List, Optional
|
||||||
|
|
||||||
from application.agents.llm_handler import get_llm_handler
|
from bson.objectid import ObjectId
|
||||||
|
|
||||||
from application.agents.tools.tool_action_parser import ToolActionParser
|
from application.agents.tools.tool_action_parser import ToolActionParser
|
||||||
from application.agents.tools.tool_manager import ToolManager
|
from application.agents.tools.tool_manager import ToolManager
|
||||||
|
|
||||||
from application.core.mongo_db import MongoDB
|
from application.core.mongo_db import MongoDB
|
||||||
|
from application.core.settings import settings
|
||||||
|
|
||||||
|
from application.llm.handlers.handler_creator import LLMHandlerCreator
|
||||||
from application.llm.llm_creator import LLMCreator
|
from application.llm.llm_creator import LLMCreator
|
||||||
from application.logging import build_stack_data, log_activity, LogContext
|
from application.logging import build_stack_data, log_activity, LogContext
|
||||||
from application.retriever.base import BaseRetriever
|
from application.retriever.base import BaseRetriever
|
||||||
from application.core.settings import settings
|
|
||||||
from bson.objectid import ObjectId
|
|
||||||
|
|
||||||
|
|
||||||
class BaseAgent(ABC):
|
class BaseAgent(ABC):
|
||||||
@@ -45,7 +47,9 @@ class BaseAgent(ABC):
|
|||||||
user_api_key=user_api_key,
|
user_api_key=user_api_key,
|
||||||
decoded_token=decoded_token,
|
decoded_token=decoded_token,
|
||||||
)
|
)
|
||||||
self.llm_handler = get_llm_handler(llm_name)
|
self.llm_handler = LLMHandlerCreator.create_handler(
|
||||||
|
llm_name if llm_name else "default"
|
||||||
|
)
|
||||||
self.attachments = attachments or []
|
self.attachments = attachments or []
|
||||||
|
|
||||||
@log_activity()
|
@log_activity()
|
||||||
@@ -132,6 +136,15 @@ class BaseAgent(ABC):
|
|||||||
parser = ToolActionParser(self.llm.__class__.__name__)
|
parser = ToolActionParser(self.llm.__class__.__name__)
|
||||||
tool_id, action_name, call_args = parser.parse_args(call)
|
tool_id, action_name, call_args = parser.parse_args(call)
|
||||||
|
|
||||||
|
call_id = getattr(call, "id", None) or str(uuid.uuid4())
|
||||||
|
tool_call_data = {
|
||||||
|
"tool_name": tools_dict[tool_id]["name"],
|
||||||
|
"call_id": call_id,
|
||||||
|
"action_name": f"{action_name}_{tool_id}",
|
||||||
|
"arguments": call_args,
|
||||||
|
}
|
||||||
|
yield {"type": "tool_call", "data": {**tool_call_data, "status": "pending"}}
|
||||||
|
|
||||||
tool_data = tools_dict[tool_id]
|
tool_data = tools_dict[tool_id]
|
||||||
action_data = (
|
action_data = (
|
||||||
tool_data["config"]["actions"][action_name]
|
tool_data["config"]["actions"][action_name]
|
||||||
@@ -184,19 +197,29 @@ class BaseAgent(ABC):
|
|||||||
else:
|
else:
|
||||||
print(f"Executing tool: {action_name} with args: {call_args}")
|
print(f"Executing tool: {action_name} with args: {call_args}")
|
||||||
result = tool.execute_action(action_name, **parameters)
|
result = tool.execute_action(action_name, **parameters)
|
||||||
call_id = getattr(call, "id", None)
|
tool_call_data["result"] = (
|
||||||
|
f"{str(result)[:50]}..." if len(str(result)) > 50 else result
|
||||||
|
)
|
||||||
|
|
||||||
tool_call_data = {
|
yield {"type": "tool_call", "data": {**tool_call_data, "status": "completed"}}
|
||||||
"tool_name": tool_data["name"],
|
|
||||||
"call_id": call_id if call_id is not None else "None",
|
|
||||||
"action_name": f"{action_name}_{tool_id}",
|
|
||||||
"arguments": call_args,
|
|
||||||
"result": result,
|
|
||||||
}
|
|
||||||
self.tool_calls.append(tool_call_data)
|
self.tool_calls.append(tool_call_data)
|
||||||
|
|
||||||
return result, call_id
|
return result, call_id
|
||||||
|
|
||||||
|
def _get_truncated_tool_calls(self):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
**tool_call,
|
||||||
|
"result": (
|
||||||
|
f"{str(tool_call['result'])[:50]}..."
|
||||||
|
if len(str(tool_call["result"])) > 50
|
||||||
|
else tool_call["result"]
|
||||||
|
),
|
||||||
|
"status": "completed",
|
||||||
|
}
|
||||||
|
for tool_call in self.tool_calls
|
||||||
|
]
|
||||||
|
|
||||||
def _build_messages(
|
def _build_messages(
|
||||||
self,
|
self,
|
||||||
system_prompt: str,
|
system_prompt: str,
|
||||||
@@ -252,9 +275,16 @@ class BaseAgent(ABC):
|
|||||||
return retrieved_data
|
return retrieved_data
|
||||||
|
|
||||||
def _llm_gen(self, messages: List[Dict], log_context: Optional[LogContext] = None):
|
def _llm_gen(self, messages: List[Dict], log_context: Optional[LogContext] = None):
|
||||||
resp = self.llm.gen_stream(
|
gen_kwargs = {"model": self.gpt_model, "messages": messages}
|
||||||
model=self.gpt_model, messages=messages, tools=self.tools
|
|
||||||
)
|
if (
|
||||||
|
hasattr(self.llm, "_supports_tools")
|
||||||
|
and self.llm._supports_tools
|
||||||
|
and self.tools
|
||||||
|
):
|
||||||
|
gen_kwargs["tools"] = self.tools
|
||||||
|
resp = self.llm.gen_stream(**gen_kwargs)
|
||||||
|
|
||||||
if log_context:
|
if log_context:
|
||||||
data = build_stack_data(self.llm, exclude_attributes=["client"])
|
data = build_stack_data(self.llm, exclude_attributes=["client"])
|
||||||
log_context.stacks.append({"component": "llm", "data": data})
|
log_context.stacks.append({"component": "llm", "data": data})
|
||||||
@@ -268,10 +298,30 @@ class BaseAgent(ABC):
|
|||||||
log_context: Optional[LogContext] = None,
|
log_context: Optional[LogContext] = None,
|
||||||
attachments: Optional[List[Dict]] = None,
|
attachments: Optional[List[Dict]] = None,
|
||||||
):
|
):
|
||||||
resp = self.llm_handler.handle_response(
|
resp = self.llm_handler.process_message_flow(
|
||||||
self, resp, tools_dict, messages, attachments
|
self, resp, tools_dict, messages, attachments, True
|
||||||
)
|
)
|
||||||
if log_context:
|
if log_context:
|
||||||
data = build_stack_data(self.llm_handler, exclude_attributes=["tool_calls"])
|
data = build_stack_data(self.llm_handler, exclude_attributes=["tool_calls"])
|
||||||
log_context.stacks.append({"component": "llm_handler", "data": data})
|
log_context.stacks.append({"component": "llm_handler", "data": data})
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
def _handle_response(self, response, tools_dict, messages, log_context):
|
||||||
|
if isinstance(response, str):
|
||||||
|
yield {"answer": response}
|
||||||
|
return
|
||||||
|
if hasattr(response, "message") and getattr(response.message, "content", None):
|
||||||
|
yield {"answer": response.message.content}
|
||||||
|
return
|
||||||
|
|
||||||
|
processed_response_gen = self._llm_handler(
|
||||||
|
response, tools_dict, messages, log_context, self.attachments
|
||||||
|
)
|
||||||
|
|
||||||
|
for event in processed_response_gen:
|
||||||
|
if isinstance(event, str):
|
||||||
|
yield {"answer": event}
|
||||||
|
elif hasattr(event, "message") and getattr(event.message, "content", None):
|
||||||
|
yield {"answer": event.message.content}
|
||||||
|
elif isinstance(event, dict) and "type" in event:
|
||||||
|
yield event
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
from typing import Dict, Generator
|
from typing import Dict, Generator
|
||||||
|
|
||||||
from application.agents.base import BaseAgent
|
from application.agents.base import BaseAgent
|
||||||
from application.logging import LogContext
|
from application.logging import LogContext
|
||||||
|
|
||||||
from application.retriever.base import BaseRetriever
|
from application.retriever.base import BaseRetriever
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -10,55 +8,46 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class ClassicAgent(BaseAgent):
|
class ClassicAgent(BaseAgent):
|
||||||
|
"""A simplified classic agent with clear execution flow.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
1. Processes a query through retrieval
|
||||||
|
2. Sets up available tools
|
||||||
|
3. Generates responses using LLM
|
||||||
|
4. Handles tool interactions if needed
|
||||||
|
5. Returns standardized outputs
|
||||||
|
|
||||||
|
Easy to extend by overriding specific steps.
|
||||||
|
"""
|
||||||
|
|
||||||
def _gen_inner(
|
def _gen_inner(
|
||||||
self, query: str, retriever: BaseRetriever, log_context: LogContext
|
self, query: str, retriever: BaseRetriever, log_context: LogContext
|
||||||
) -> Generator[Dict, None, None]:
|
) -> Generator[Dict, None, None]:
|
||||||
|
# Step 1: Retrieve relevant data
|
||||||
retrieved_data = self._retriever_search(retriever, query, log_context)
|
retrieved_data = self._retriever_search(retriever, query, log_context)
|
||||||
if self.user_api_key:
|
|
||||||
tools_dict = self._get_tools(self.user_api_key)
|
# Step 2: Prepare tools
|
||||||
else:
|
tools_dict = (
|
||||||
tools_dict = self._get_user_tools(self.user)
|
self._get_user_tools(self.user)
|
||||||
|
if not self.user_api_key
|
||||||
|
else self._get_tools(self.user_api_key)
|
||||||
|
)
|
||||||
self._prepare_tools(tools_dict)
|
self._prepare_tools(tools_dict)
|
||||||
|
|
||||||
|
# Step 3: Build and process messages
|
||||||
messages = self._build_messages(self.prompt, query, retrieved_data)
|
messages = self._build_messages(self.prompt, query, retrieved_data)
|
||||||
|
llm_response = self._llm_gen(messages, log_context)
|
||||||
|
|
||||||
resp = self._llm_gen(messages, log_context)
|
# Step 4: Handle the response
|
||||||
|
yield from self._handle_response(
|
||||||
|
llm_response, tools_dict, messages, log_context
|
||||||
|
)
|
||||||
|
|
||||||
attachments = self.attachments
|
# Step 5: Return metadata
|
||||||
|
yield {"sources": retrieved_data}
|
||||||
if isinstance(resp, str):
|
yield {"tool_calls": self._get_truncated_tool_calls()}
|
||||||
yield {"answer": resp}
|
|
||||||
return
|
|
||||||
if (
|
|
||||||
hasattr(resp, "message")
|
|
||||||
and hasattr(resp.message, "content")
|
|
||||||
and resp.message.content is not None
|
|
||||||
):
|
|
||||||
yield {"answer": resp.message.content}
|
|
||||||
return
|
|
||||||
|
|
||||||
resp = self._llm_handler(resp, tools_dict, messages, log_context, attachments)
|
|
||||||
|
|
||||||
if isinstance(resp, str):
|
|
||||||
yield {"answer": resp}
|
|
||||||
elif (
|
|
||||||
hasattr(resp, "message")
|
|
||||||
and hasattr(resp.message, "content")
|
|
||||||
and resp.message.content is not None
|
|
||||||
):
|
|
||||||
yield {"answer": resp.message.content}
|
|
||||||
else:
|
|
||||||
for line in resp:
|
|
||||||
if isinstance(line, str):
|
|
||||||
yield {"answer": line}
|
|
||||||
|
|
||||||
|
# Log tool calls for debugging
|
||||||
log_context.stacks.append(
|
log_context.stacks.append(
|
||||||
{"component": "agent", "data": {"tool_calls": self.tool_calls.copy()}}
|
{"component": "agent", "data": {"tool_calls": self.tool_calls.copy()}}
|
||||||
)
|
)
|
||||||
|
|
||||||
yield {"sources": retrieved_data}
|
|
||||||
# clean tool_call_data only send first 50 characters of tool_call['result']
|
|
||||||
for tool_call in self.tool_calls:
|
|
||||||
if len(str(tool_call["result"])) > 50:
|
|
||||||
tool_call["result"] = str(tool_call["result"])[:50] + "..."
|
|
||||||
yield {"tool_calls": self.tool_calls.copy()}
|
|
||||||
|
|||||||
@@ -1,351 +0,0 @@
|
|||||||
import json
|
|
||||||
import logging
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
from application.logging import build_stack_data
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class LLMHandler(ABC):
|
|
||||||
def __init__(self):
|
|
||||||
self.llm_calls = []
|
|
||||||
self.tool_calls = []
|
|
||||||
|
|
||||||
@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.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
agent: The current agent instance.
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
if not attachments:
|
|
||||||
return messages
|
|
||||||
|
|
||||||
logger.info(f"Preparing messages with {len(attachments)} attachments")
|
|
||||||
|
|
||||||
supported_types = agent.llm.get_supported_attachment_types()
|
|
||||||
|
|
||||||
supported_attachments = []
|
|
||||||
unsupported_attachments = []
|
|
||||||
|
|
||||||
for attachment in attachments:
|
|
||||||
mime_type = attachment.get('mime_type')
|
|
||||||
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()
|
|
||||||
|
|
||||||
attachment_texts = []
|
|
||||||
for attachment in attachments:
|
|
||||||
logger.info(f"Adding attachment {attachment.get('id')} to context")
|
|
||||||
if 'content' in attachment:
|
|
||||||
attachment_texts.append(f"Attached file content:\n\n{attachment['content']}")
|
|
||||||
|
|
||||||
if attachment_texts:
|
|
||||||
combined_attachment_text = "\n\n".join(attachment_texts)
|
|
||||||
|
|
||||||
system_found = False
|
|
||||||
for i in range(len(prepared_messages)):
|
|
||||||
if prepared_messages[i].get("role") == "system":
|
|
||||||
prepared_messages[i]["content"] += f"\n\n{combined_attachment_text}"
|
|
||||||
system_found = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not system_found:
|
|
||||||
prepared_messages.insert(0, {"role": "system", "content": combined_attachment_text})
|
|
||||||
|
|
||||||
return prepared_messages
|
|
||||||
|
|
||||||
class OpenAILLMHandler(LLMHandler):
|
|
||||||
def handle_response(self, agent, resp, tools_dict, messages, attachments=None, stream: bool = True):
|
|
||||||
|
|
||||||
messages = self.prepare_messages_with_attachments(agent, messages, attachments)
|
|
||||||
logger.info(f"Messages with attachments: {messages}")
|
|
||||||
if not stream:
|
|
||||||
while hasattr(resp, "finish_reason") and resp.finish_reason == "tool_calls":
|
|
||||||
message = json.loads(resp.model_dump_json())["message"]
|
|
||||||
keys_to_remove = {"audio", "function_call", "refusal"}
|
|
||||||
filtered_data = {
|
|
||||||
k: v for k, v in message.items() if k not in keys_to_remove
|
|
||||||
}
|
|
||||||
messages.append(filtered_data)
|
|
||||||
|
|
||||||
tool_calls = resp.message.tool_calls
|
|
||||||
for call in tool_calls:
|
|
||||||
try:
|
|
||||||
self.tool_calls.append(call)
|
|
||||||
tool_response, call_id = agent._execute_tool_action(
|
|
||||||
tools_dict, call
|
|
||||||
)
|
|
||||||
function_call_dict = {
|
|
||||||
"function_call": {
|
|
||||||
"name": call.function.name,
|
|
||||||
"args": call.function.arguments,
|
|
||||||
"call_id": call_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function_response_dict = {
|
|
||||||
"function_response": {
|
|
||||||
"name": call.function.name,
|
|
||||||
"response": {"result": tool_response},
|
|
||||||
"call_id": call_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.append(
|
|
||||||
{"role": "assistant", "content": [function_call_dict]}
|
|
||||||
)
|
|
||||||
messages.append(
|
|
||||||
{"role": "tool", "content": [function_response_dict]}
|
|
||||||
)
|
|
||||||
|
|
||||||
messages = self.prepare_messages_with_attachments(agent, messages, attachments)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error executing tool: {str(e)}", exc_info=True)
|
|
||||||
messages.append(
|
|
||||||
{
|
|
||||||
"role": "tool",
|
|
||||||
"content": f"Error executing tool: {str(e)}",
|
|
||||||
"tool_call_id": call_id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
resp = agent.llm.gen_stream(
|
|
||||||
model=agent.gpt_model, messages=messages, tools=agent.tools
|
|
||||||
)
|
|
||||||
self.llm_calls.append(build_stack_data(agent.llm))
|
|
||||||
return resp
|
|
||||||
|
|
||||||
else:
|
|
||||||
text_buffer = ""
|
|
||||||
while True:
|
|
||||||
tool_calls = {}
|
|
||||||
for chunk in resp:
|
|
||||||
if isinstance(chunk, str) and len(chunk) > 0:
|
|
||||||
yield chunk
|
|
||||||
continue
|
|
||||||
elif hasattr(chunk, "delta"):
|
|
||||||
chunk_delta = chunk.delta
|
|
||||||
|
|
||||||
if (
|
|
||||||
hasattr(chunk_delta, "tool_calls")
|
|
||||||
and chunk_delta.tool_calls is not None
|
|
||||||
):
|
|
||||||
for tool_call in chunk_delta.tool_calls:
|
|
||||||
index = tool_call.index
|
|
||||||
if index not in tool_calls:
|
|
||||||
tool_calls[index] = {
|
|
||||||
"id": "",
|
|
||||||
"function": {"name": "", "arguments": ""},
|
|
||||||
}
|
|
||||||
|
|
||||||
current = tool_calls[index]
|
|
||||||
if tool_call.id:
|
|
||||||
current["id"] = tool_call.id
|
|
||||||
if tool_call.function.name:
|
|
||||||
current["function"][
|
|
||||||
"name"
|
|
||||||
] = tool_call.function.name
|
|
||||||
if tool_call.function.arguments:
|
|
||||||
current["function"][
|
|
||||||
"arguments"
|
|
||||||
] += tool_call.function.arguments
|
|
||||||
tool_calls[index] = current
|
|
||||||
|
|
||||||
if (
|
|
||||||
hasattr(chunk, "finish_reason")
|
|
||||||
and chunk.finish_reason == "tool_calls"
|
|
||||||
):
|
|
||||||
for index in sorted(tool_calls.keys()):
|
|
||||||
call = tool_calls[index]
|
|
||||||
try:
|
|
||||||
self.tool_calls.append(call)
|
|
||||||
tool_response, call_id = agent._execute_tool_action(
|
|
||||||
tools_dict, call
|
|
||||||
)
|
|
||||||
if isinstance(call["function"]["arguments"], str):
|
|
||||||
call["function"]["arguments"] = json.loads(call["function"]["arguments"])
|
|
||||||
|
|
||||||
function_call_dict = {
|
|
||||||
"function_call": {
|
|
||||||
"name": call["function"]["name"],
|
|
||||||
"args": call["function"]["arguments"],
|
|
||||||
"call_id": call["id"],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function_response_dict = {
|
|
||||||
"function_response": {
|
|
||||||
"name": call["function"]["name"],
|
|
||||||
"response": {"result": tool_response},
|
|
||||||
"call_id": call["id"],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.append(
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": [function_call_dict],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
messages.append(
|
|
||||||
{
|
|
||||||
"role": "tool",
|
|
||||||
"content": [function_response_dict],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error executing tool: {str(e)}", exc_info=True)
|
|
||||||
messages.append(
|
|
||||||
{
|
|
||||||
"role": "assistant",
|
|
||||||
"content": f"Error executing tool: {str(e)}",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
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 resp
|
|
||||||
elif isinstance(chunk, str) and len(chunk) == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
logger.info(f"Regenerating with messages: {messages}")
|
|
||||||
resp = agent.llm.gen_stream(
|
|
||||||
model=agent.gpt_model, messages=messages, tools=agent.tools
|
|
||||||
)
|
|
||||||
self.llm_calls.append(build_stack_data(agent.llm))
|
|
||||||
|
|
||||||
|
|
||||||
class GoogleLLMHandler(LLMHandler):
|
|
||||||
def handle_response(self, agent, resp, tools_dict, messages, attachments=None, stream: bool = True):
|
|
||||||
from google.genai import types
|
|
||||||
|
|
||||||
messages = self.prepare_messages_with_attachments(agent, messages, attachments)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
if not stream:
|
|
||||||
response = agent.llm.gen(
|
|
||||||
model=agent.gpt_model, messages=messages, tools=agent.tools
|
|
||||||
)
|
|
||||||
self.llm_calls.append(build_stack_data(agent.llm))
|
|
||||||
if response.candidates and response.candidates[0].content.parts:
|
|
||||||
tool_call_found = False
|
|
||||||
for part in response.candidates[0].content.parts:
|
|
||||||
if part.function_call:
|
|
||||||
tool_call_found = True
|
|
||||||
self.tool_calls.append(part.function_call)
|
|
||||||
tool_response, call_id = agent._execute_tool_action(
|
|
||||||
tools_dict, part.function_call
|
|
||||||
)
|
|
||||||
function_response_part = types.Part.from_function_response(
|
|
||||||
name=part.function_call.name,
|
|
||||||
response={"result": tool_response},
|
|
||||||
)
|
|
||||||
|
|
||||||
messages.append(
|
|
||||||
{"role": "model", "content": [part.to_json_dict()]}
|
|
||||||
)
|
|
||||||
messages.append(
|
|
||||||
{
|
|
||||||
"role": "tool",
|
|
||||||
"content": [function_response_part.to_json_dict()],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
not tool_call_found
|
|
||||||
and response.candidates[0].content.parts
|
|
||||||
and response.candidates[0].content.parts[0].text
|
|
||||||
):
|
|
||||||
return response.candidates[0].content.parts[0].text
|
|
||||||
elif not tool_call_found:
|
|
||||||
return response.candidates[0].content.parts
|
|
||||||
|
|
||||||
else:
|
|
||||||
return response
|
|
||||||
|
|
||||||
else:
|
|
||||||
response = agent.llm.gen_stream(
|
|
||||||
model=agent.gpt_model, messages=messages, tools=agent.tools
|
|
||||||
)
|
|
||||||
self.llm_calls.append(build_stack_data(agent.llm))
|
|
||||||
|
|
||||||
tool_call_found = False
|
|
||||||
for result in response:
|
|
||||||
if hasattr(result, "function_call"):
|
|
||||||
tool_call_found = True
|
|
||||||
self.tool_calls.append(result.function_call)
|
|
||||||
tool_response, call_id = agent._execute_tool_action(
|
|
||||||
tools_dict, result.function_call
|
|
||||||
)
|
|
||||||
function_response_part = types.Part.from_function_response(
|
|
||||||
name=result.function_call.name,
|
|
||||||
response={"result": tool_response},
|
|
||||||
)
|
|
||||||
|
|
||||||
messages.append(
|
|
||||||
{"role": "model", "content": [result.to_json_dict()]}
|
|
||||||
)
|
|
||||||
messages.append(
|
|
||||||
{
|
|
||||||
"role": "tool",
|
|
||||||
"content": [function_response_part.to_json_dict()],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
tool_call_found = False
|
|
||||||
yield result
|
|
||||||
|
|
||||||
if not tool_call_found:
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
def get_llm_handler(llm_type):
|
|
||||||
handlers = {
|
|
||||||
"openai": OpenAILLMHandler(),
|
|
||||||
"google": GoogleLLMHandler(),
|
|
||||||
}
|
|
||||||
return handlers.get(llm_type, OpenAILLMHandler())
|
|
||||||
@@ -17,26 +17,21 @@ class ToolActionParser:
|
|||||||
return parser(call)
|
return parser(call)
|
||||||
|
|
||||||
def _parse_openai_llm(self, call):
|
def _parse_openai_llm(self, call):
|
||||||
if isinstance(call, dict):
|
try:
|
||||||
try:
|
call_args = json.loads(call.arguments)
|
||||||
call_args = json.loads(call["function"]["arguments"])
|
tool_id = call.name.split("_")[-1]
|
||||||
tool_id = call["function"]["name"].split("_")[-1]
|
action_name = call.name.rsplit("_", 1)[0]
|
||||||
action_name = call["function"]["name"].rsplit("_", 1)[0]
|
except (AttributeError, TypeError) as e:
|
||||||
except (KeyError, TypeError) as e:
|
logger.error(f"Error parsing OpenAI LLM call: {e}")
|
||||||
logger.error(f"Error parsing OpenAI LLM call: {e}")
|
return None, None, None
|
||||||
return None, None, None
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
call_args = json.loads(call.function.arguments)
|
|
||||||
tool_id = call.function.name.split("_")[-1]
|
|
||||||
action_name = call.function.name.rsplit("_", 1)[0]
|
|
||||||
except (AttributeError, TypeError) as e:
|
|
||||||
logger.error(f"Error parsing OpenAI LLM call: {e}")
|
|
||||||
return None, None, None
|
|
||||||
return tool_id, action_name, call_args
|
return tool_id, action_name, call_args
|
||||||
|
|
||||||
def _parse_google_llm(self, call):
|
def _parse_google_llm(self, call):
|
||||||
call_args = call.args
|
try:
|
||||||
tool_id = call.name.split("_")[-1]
|
call_args = call.arguments
|
||||||
action_name = call.name.rsplit("_", 1)[0]
|
tool_id = call.name.split("_")[-1]
|
||||||
|
action_name = call.name.rsplit("_", 1)[0]
|
||||||
|
except (AttributeError, TypeError) as e:
|
||||||
|
logger.error(f"Error parsing Google LLM call: {e}")
|
||||||
|
return None, None, None
|
||||||
return tool_id, action_name, call_args
|
return tool_id, action_name, call_args
|
||||||
|
|||||||
@@ -37,17 +37,17 @@ api.add_namespace(answer_ns)
|
|||||||
|
|
||||||
gpt_model = ""
|
gpt_model = ""
|
||||||
# to have some kind of default behaviour
|
# to have some kind of default behaviour
|
||||||
if settings.LLM_NAME == "openai":
|
if settings.LLM_PROVIDER == "openai":
|
||||||
gpt_model = "gpt-4o-mini"
|
gpt_model = "gpt-4o-mini"
|
||||||
elif settings.LLM_NAME == "anthropic":
|
elif settings.LLM_PROVIDER == "anthropic":
|
||||||
gpt_model = "claude-2"
|
gpt_model = "claude-2"
|
||||||
elif settings.LLM_NAME == "groq":
|
elif settings.LLM_PROVIDER == "groq":
|
||||||
gpt_model = "llama3-8b-8192"
|
gpt_model = "llama3-8b-8192"
|
||||||
elif settings.LLM_NAME == "novita":
|
elif settings.LLM_PROVIDER == "novita":
|
||||||
gpt_model = "deepseek/deepseek-r1"
|
gpt_model = "deepseek/deepseek-r1"
|
||||||
|
|
||||||
if settings.MODEL_NAME: # in case there is particular model name configured
|
if settings.LLM_NAME: # in case there is particular model name configured
|
||||||
gpt_model = settings.MODEL_NAME
|
gpt_model = settings.LLM_NAME
|
||||||
|
|
||||||
# load the prompts
|
# load the prompts
|
||||||
current_dir = os.path.dirname(
|
current_dir = os.path.dirname(
|
||||||
@@ -164,6 +164,7 @@ def save_conversation(
|
|||||||
agent_id=None,
|
agent_id=None,
|
||||||
is_shared_usage=False,
|
is_shared_usage=False,
|
||||||
shared_token=None,
|
shared_token=None,
|
||||||
|
attachment_ids=None,
|
||||||
):
|
):
|
||||||
current_time = datetime.datetime.now(datetime.timezone.utc)
|
current_time = datetime.datetime.now(datetime.timezone.utc)
|
||||||
if conversation_id is not None and index is not None:
|
if conversation_id is not None and index is not None:
|
||||||
@@ -177,6 +178,7 @@ def save_conversation(
|
|||||||
f"queries.{index}.sources": source_log_docs,
|
f"queries.{index}.sources": source_log_docs,
|
||||||
f"queries.{index}.tool_calls": tool_calls,
|
f"queries.{index}.tool_calls": tool_calls,
|
||||||
f"queries.{index}.timestamp": current_time,
|
f"queries.{index}.timestamp": current_time,
|
||||||
|
f"queries.{index}.attachments": attachment_ids,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -197,6 +199,7 @@ def save_conversation(
|
|||||||
"sources": source_log_docs,
|
"sources": source_log_docs,
|
||||||
"tool_calls": tool_calls,
|
"tool_calls": tool_calls,
|
||||||
"timestamp": current_time,
|
"timestamp": current_time,
|
||||||
|
"attachments": attachment_ids,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -233,6 +236,7 @@ def save_conversation(
|
|||||||
"sources": source_log_docs,
|
"sources": source_log_docs,
|
||||||
"tool_calls": tool_calls,
|
"tool_calls": tool_calls,
|
||||||
"timestamp": current_time,
|
"timestamp": current_time,
|
||||||
|
"attachments": attachment_ids,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -273,20 +277,13 @@ def complete_stream(
|
|||||||
isNoneDoc=False,
|
isNoneDoc=False,
|
||||||
index=None,
|
index=None,
|
||||||
should_save_conversation=True,
|
should_save_conversation=True,
|
||||||
attachments=None,
|
attachment_ids=None,
|
||||||
agent_id=None,
|
agent_id=None,
|
||||||
is_shared_usage=False,
|
is_shared_usage=False,
|
||||||
shared_token=None,
|
shared_token=None,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
response_full, thought, source_log_docs, tool_calls = "", "", [], []
|
response_full, thought, source_log_docs, tool_calls = "", "", [], []
|
||||||
attachment_ids = []
|
|
||||||
|
|
||||||
if attachments:
|
|
||||||
attachment_ids = [attachment["id"] for attachment in attachments]
|
|
||||||
logger.info(
|
|
||||||
f"Processing request with {len(attachments)} attachments: {attachment_ids}"
|
|
||||||
)
|
|
||||||
|
|
||||||
answer = agent.gen(query=question, retriever=retriever)
|
answer = agent.gen(query=question, retriever=retriever)
|
||||||
|
|
||||||
@@ -310,19 +307,20 @@ def complete_stream(
|
|||||||
yield f"data: {data}\n\n"
|
yield f"data: {data}\n\n"
|
||||||
elif "tool_calls" in line:
|
elif "tool_calls" in line:
|
||||||
tool_calls = line["tool_calls"]
|
tool_calls = line["tool_calls"]
|
||||||
data = json.dumps({"type": "tool_calls", "tool_calls": tool_calls})
|
|
||||||
yield f"data: {data}\n\n"
|
|
||||||
elif "thought" in line:
|
elif "thought" in line:
|
||||||
thought += line["thought"]
|
thought += line["thought"]
|
||||||
data = json.dumps({"type": "thought", "thought": line["thought"]})
|
data = json.dumps({"type": "thought", "thought": line["thought"]})
|
||||||
yield f"data: {data}\n\n"
|
yield f"data: {data}\n\n"
|
||||||
|
elif "type" in line:
|
||||||
|
data = json.dumps(line)
|
||||||
|
yield f"data: {data}\n\n"
|
||||||
|
|
||||||
if isNoneDoc:
|
if isNoneDoc:
|
||||||
for doc in source_log_docs:
|
for doc in source_log_docs:
|
||||||
doc["source"] = "None"
|
doc["source"] = "None"
|
||||||
|
|
||||||
llm = LLMCreator.create_llm(
|
llm = LLMCreator.create_llm(
|
||||||
settings.LLM_NAME,
|
settings.LLM_PROVIDER,
|
||||||
api_key=settings.API_KEY,
|
api_key=settings.API_KEY,
|
||||||
user_api_key=user_api_key,
|
user_api_key=user_api_key,
|
||||||
decoded_token=decoded_token,
|
decoded_token=decoded_token,
|
||||||
@@ -340,6 +338,7 @@ def complete_stream(
|
|||||||
decoded_token,
|
decoded_token,
|
||||||
index,
|
index,
|
||||||
api_key=user_api_key,
|
api_key=user_api_key,
|
||||||
|
attachment_ids=attachment_ids,
|
||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
is_shared_usage=is_shared_usage,
|
is_shared_usage=is_shared_usage,
|
||||||
shared_token=shared_token,
|
shared_token=shared_token,
|
||||||
@@ -453,9 +452,7 @@ class Stream(Resource):
|
|||||||
agent_type = settings.AGENT_NAME
|
agent_type = settings.AGENT_NAME
|
||||||
decoded_token = getattr(request, "decoded_token", None)
|
decoded_token = getattr(request, "decoded_token", None)
|
||||||
user_sub = decoded_token.get("sub") if decoded_token else None
|
user_sub = decoded_token.get("sub") if decoded_token else None
|
||||||
agent_key, is_shared_usage, shared_token = get_agent_key(
|
agent_key, is_shared_usage, shared_token = get_agent_key(agent_id, user_sub)
|
||||||
agent_id, user_sub
|
|
||||||
)
|
|
||||||
|
|
||||||
if agent_key:
|
if agent_key:
|
||||||
data.update({"api_key": agent_key})
|
data.update({"api_key": agent_key})
|
||||||
@@ -506,7 +503,7 @@ class Stream(Resource):
|
|||||||
agent = AgentCreator.create_agent(
|
agent = AgentCreator.create_agent(
|
||||||
agent_type,
|
agent_type,
|
||||||
endpoint="stream",
|
endpoint="stream",
|
||||||
llm_name=settings.LLM_NAME,
|
llm_name=settings.LLM_PROVIDER,
|
||||||
gpt_model=gpt_model,
|
gpt_model=gpt_model,
|
||||||
api_key=settings.API_KEY,
|
api_key=settings.API_KEY,
|
||||||
user_api_key=user_api_key,
|
user_api_key=user_api_key,
|
||||||
@@ -539,6 +536,7 @@ class Stream(Resource):
|
|||||||
isNoneDoc=data.get("isNoneDoc"),
|
isNoneDoc=data.get("isNoneDoc"),
|
||||||
index=index,
|
index=index,
|
||||||
should_save_conversation=save_conv,
|
should_save_conversation=save_conv,
|
||||||
|
attachment_ids=attachment_ids,
|
||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
is_shared_usage=is_shared_usage,
|
is_shared_usage=is_shared_usage,
|
||||||
shared_token=shared_token,
|
shared_token=shared_token,
|
||||||
@@ -659,7 +657,7 @@ class Answer(Resource):
|
|||||||
agent = AgentCreator.create_agent(
|
agent = AgentCreator.create_agent(
|
||||||
agent_type,
|
agent_type,
|
||||||
endpoint="api/answer",
|
endpoint="api/answer",
|
||||||
llm_name=settings.LLM_NAME,
|
llm_name=settings.LLM_PROVIDER,
|
||||||
gpt_model=gpt_model,
|
gpt_model=gpt_model,
|
||||||
api_key=settings.API_KEY,
|
api_key=settings.API_KEY,
|
||||||
user_api_key=user_api_key,
|
user_api_key=user_api_key,
|
||||||
@@ -728,7 +726,7 @@ class Answer(Resource):
|
|||||||
doc["source"] = "None"
|
doc["source"] = "None"
|
||||||
|
|
||||||
llm = LLMCreator.create_llm(
|
llm = LLMCreator.create_llm(
|
||||||
settings.LLM_NAME,
|
settings.LLM_PROVIDER,
|
||||||
api_key=settings.API_KEY,
|
api_key=settings.API_KEY,
|
||||||
user_api_key=user_api_key,
|
user_api_key=user_api_key,
|
||||||
decoded_token=decoded_token,
|
decoded_token=decoded_token,
|
||||||
|
|||||||
@@ -37,16 +37,18 @@ def upload_index_files():
|
|||||||
"""Upload two files(index.faiss, index.pkl) to the user's folder."""
|
"""Upload two files(index.faiss, index.pkl) to the user's folder."""
|
||||||
if "user" not in request.form:
|
if "user" not in request.form:
|
||||||
return {"status": "no user"}
|
return {"status": "no user"}
|
||||||
user = secure_filename(request.form["user"])
|
user = request.form["user"]
|
||||||
if "name" not in request.form:
|
if "name" not in request.form:
|
||||||
return {"status": "no name"}
|
return {"status": "no name"}
|
||||||
job_name = secure_filename(request.form["name"])
|
job_name = request.form["name"]
|
||||||
tokens = secure_filename(request.form["tokens"])
|
tokens = request.form["tokens"]
|
||||||
retriever = secure_filename(request.form["retriever"])
|
retriever = request.form["retriever"]
|
||||||
id = secure_filename(request.form["id"])
|
id = request.form["id"]
|
||||||
type = secure_filename(request.form["type"])
|
type = request.form["type"]
|
||||||
remote_data = request.form["remote_data"] if "remote_data" in request.form else None
|
remote_data = request.form["remote_data"] if "remote_data" in request.form else None
|
||||||
sync_frequency = secure_filename(request.form["sync_frequency"]) if "sync_frequency" in request.form else None
|
sync_frequency = request.form["sync_frequency"] if "sync_frequency" in request.form else None
|
||||||
|
|
||||||
|
original_file_path = request.form.get("original_file_path")
|
||||||
|
|
||||||
storage = StorageCreator.get_storage()
|
storage = StorageCreator.get_storage()
|
||||||
index_base_path = f"indexes/{id}"
|
index_base_path = f"indexes/{id}"
|
||||||
@@ -85,6 +87,7 @@ def upload_index_files():
|
|||||||
"retriever": retriever,
|
"retriever": retriever,
|
||||||
"remote_data": remote_data,
|
"remote_data": remote_data,
|
||||||
"sync_frequency": sync_frequency,
|
"sync_frequency": sync_frequency,
|
||||||
|
"file_path": original_file_path,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -102,6 +105,7 @@ def upload_index_files():
|
|||||||
"retriever": retriever,
|
"retriever": retriever,
|
||||||
"remote_data": remote_data,
|
"remote_data": remote_data,
|
||||||
"sync_frequency": sync_frequency,
|
"sync_frequency": sync_frequency,
|
||||||
|
"file_path": original_file_path,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|||||||
@@ -6,16 +6,25 @@ import secrets
|
|||||||
import shutil
|
import shutil
|
||||||
import uuid
|
import uuid
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
from bson.binary import Binary, UuidRepresentation
|
from bson.binary import Binary, UuidRepresentation
|
||||||
from bson.dbref import DBRef
|
from bson.dbref import DBRef
|
||||||
from bson.objectid import ObjectId
|
from bson.objectid import ObjectId
|
||||||
from flask import Blueprint, current_app, jsonify, make_response, redirect, request
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
|
current_app,
|
||||||
|
jsonify,
|
||||||
|
make_response,
|
||||||
|
redirect,
|
||||||
|
request,
|
||||||
|
Response,
|
||||||
|
)
|
||||||
from flask_restx import fields, inputs, Namespace, Resource
|
from flask_restx import fields, inputs, Namespace, Resource
|
||||||
|
from pymongo import ReturnDocument
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
from application.agents.tools.tool_manager import ToolManager
|
from application.agents.tools.tool_manager import ToolManager
|
||||||
from pymongo import ReturnDocument
|
|
||||||
|
|
||||||
from application.api.user.tasks import (
|
from application.api.user.tasks import (
|
||||||
ingest,
|
ingest,
|
||||||
@@ -28,7 +37,12 @@ from application.core.settings import settings
|
|||||||
from application.extensions import api
|
from application.extensions import api
|
||||||
from application.storage.storage_creator import StorageCreator
|
from application.storage.storage_creator import StorageCreator
|
||||||
from application.tts.google_tts import GoogleTTS
|
from application.tts.google_tts import GoogleTTS
|
||||||
from application.utils import check_required_fields, validate_function_name
|
from application.utils import (
|
||||||
|
check_required_fields,
|
||||||
|
generate_image_url,
|
||||||
|
safe_filename,
|
||||||
|
validate_function_name,
|
||||||
|
)
|
||||||
from application.vectorstore.vector_creator import VectorCreator
|
from application.vectorstore.vector_creator import VectorCreator
|
||||||
|
|
||||||
storage = StorageCreator.get_storage()
|
storage = StorageCreator.get_storage()
|
||||||
@@ -45,6 +59,7 @@ shared_conversations_collections = db["shared_conversations"]
|
|||||||
users_collection = db["users"]
|
users_collection = db["users"]
|
||||||
user_logs_collection = db["user_logs"]
|
user_logs_collection = db["user_logs"]
|
||||||
user_tools_collection = db["user_tools"]
|
user_tools_collection = db["user_tools"]
|
||||||
|
attachments_collection = db["attachments"]
|
||||||
|
|
||||||
agents_collection.create_index(
|
agents_collection.create_index(
|
||||||
[("shared", 1)],
|
[("shared", 1)],
|
||||||
@@ -142,6 +157,29 @@ def get_vector_store(source_id):
|
|||||||
return store
|
return store
|
||||||
|
|
||||||
|
|
||||||
|
def handle_image_upload(
|
||||||
|
request, existing_url: str, user: str, storage, base_path: str = "attachments/"
|
||||||
|
) -> Tuple[str, Optional[Response]]:
|
||||||
|
image_url = existing_url
|
||||||
|
|
||||||
|
if "image" in request.files:
|
||||||
|
file = request.files["image"]
|
||||||
|
if file.filename != "":
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
upload_path = f"{settings.UPLOAD_FOLDER.rstrip('/')}/{user}/{base_path.rstrip('/')}/{uuid.uuid4()}_{filename}"
|
||||||
|
try:
|
||||||
|
storage.save_file(file, upload_path)
|
||||||
|
image_url = upload_path
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Error uploading image: {e}")
|
||||||
|
return None, make_response(
|
||||||
|
jsonify({"success": False, "message": "Image upload failed"}),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
|
return image_url, None
|
||||||
|
|
||||||
|
|
||||||
@user_ns.route("/api/delete_conversation")
|
@user_ns.route("/api/delete_conversation")
|
||||||
class DeleteConversation(Resource):
|
class DeleteConversation(Resource):
|
||||||
@api.doc(
|
@api.doc(
|
||||||
@@ -252,13 +290,39 @@ class GetSingleConversation(Resource):
|
|||||||
)
|
)
|
||||||
if not conversation:
|
if not conversation:
|
||||||
return make_response(jsonify({"status": "not found"}), 404)
|
return make_response(jsonify({"status": "not found"}), 404)
|
||||||
|
|
||||||
|
# Process queries to include attachment names
|
||||||
|
queries = conversation["queries"]
|
||||||
|
for query in queries:
|
||||||
|
if "attachments" in query and query["attachments"]:
|
||||||
|
attachment_details = []
|
||||||
|
for attachment_id in query["attachments"]:
|
||||||
|
try:
|
||||||
|
attachment = attachments_collection.find_one(
|
||||||
|
{"_id": ObjectId(attachment_id)}
|
||||||
|
)
|
||||||
|
if attachment:
|
||||||
|
attachment_details.append(
|
||||||
|
{
|
||||||
|
"id": str(attachment["_id"]),
|
||||||
|
"fileName": attachment.get(
|
||||||
|
"filename", "Unknown file"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(
|
||||||
|
f"Error retrieving attachment {attachment_id}: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
query["attachments"] = attachment_details
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
current_app.logger.error(
|
current_app.logger.error(
|
||||||
f"Error retrieving conversation: {err}", exc_info=True
|
f"Error retrieving conversation: {err}", exc_info=True
|
||||||
)
|
)
|
||||||
return make_response(jsonify({"success": False}), 400)
|
return make_response(jsonify({"success": False}), 400)
|
||||||
data = {
|
data = {
|
||||||
"queries": conversation["queries"],
|
"queries": queries,
|
||||||
"agent_id": conversation.get("agent_id"),
|
"agent_id": conversation.get("agent_id"),
|
||||||
"is_shared_usage": conversation.get("is_shared_usage", False),
|
"is_shared_usage": conversation.get("is_shared_usage", False),
|
||||||
"shared_token": conversation.get("shared_token", None),
|
"shared_token": conversation.get("shared_token", None),
|
||||||
@@ -475,29 +539,30 @@ class UploadFile(Resource):
|
|||||||
),
|
),
|
||||||
400,
|
400,
|
||||||
)
|
)
|
||||||
user = secure_filename(decoded_token.get("sub"))
|
user = decoded_token.get("sub")
|
||||||
job_name = secure_filename(request.form["name"])
|
job_name = request.form["name"]
|
||||||
|
|
||||||
|
# Create safe versions for filesystem operations
|
||||||
|
safe_user = safe_filename(user)
|
||||||
|
dir_name = safe_filename(job_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from application.storage.storage_creator import StorageCreator
|
|
||||||
|
|
||||||
storage = StorageCreator.get_storage()
|
storage = StorageCreator.get_storage()
|
||||||
|
base_path = f"{settings.UPLOAD_FOLDER}/{safe_user}/{dir_name}"
|
||||||
base_path = f"{settings.UPLOAD_FOLDER}/{user}/{job_name}"
|
|
||||||
|
|
||||||
if len(files) > 1:
|
if len(files) > 1:
|
||||||
temp_files = []
|
temp_files = []
|
||||||
for file in files:
|
for file in files:
|
||||||
filename = secure_filename(file.filename)
|
filename = safe_filename(file.filename)
|
||||||
temp_path = f"{base_path}/temp/{filename}"
|
temp_path = f"{base_path}/temp/{filename}"
|
||||||
storage.save_file(file, temp_path)
|
storage.save_file(file, temp_path)
|
||||||
temp_files.append(temp_path)
|
temp_files.append(temp_path)
|
||||||
print(f"Saved file: {filename}")
|
print(f"Saved file: {filename}")
|
||||||
zip_filename = f"{job_name}.zip"
|
zip_filename = f"{dir_name}.zip"
|
||||||
zip_path = f"{base_path}/{zip_filename}"
|
zip_path = f"{base_path}/{zip_filename}"
|
||||||
zip_temp_path = None
|
zip_temp_path = None
|
||||||
|
|
||||||
def create_zip_archive(temp_paths, job_name, storage):
|
def create_zip_archive(temp_paths, dir_name, storage):
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(
|
with tempfile.NamedTemporaryFile(
|
||||||
@@ -537,7 +602,7 @@ class UploadFile(Resource):
|
|||||||
return zip_output_path
|
return zip_output_path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
zip_temp_path = create_zip_archive(temp_files, job_name, storage)
|
zip_temp_path = create_zip_archive(temp_files, dir_name, storage)
|
||||||
with open(zip_temp_path, "rb") as zip_file:
|
with open(zip_temp_path, "rb") as zip_file:
|
||||||
storage.save_file(zip_file, zip_path)
|
storage.save_file(zip_file, zip_path)
|
||||||
task = ingest.delay(
|
task = ingest.delay(
|
||||||
@@ -562,6 +627,8 @@ class UploadFile(Resource):
|
|||||||
job_name,
|
job_name,
|
||||||
zip_filename,
|
zip_filename,
|
||||||
user,
|
user,
|
||||||
|
dir_name,
|
||||||
|
safe_user,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
# Clean up temporary files
|
# Clean up temporary files
|
||||||
@@ -582,7 +649,7 @@ class UploadFile(Resource):
|
|||||||
# For single file
|
# For single file
|
||||||
|
|
||||||
file = files[0]
|
file = files[0]
|
||||||
filename = secure_filename(file.filename)
|
filename = safe_filename(file.filename)
|
||||||
file_path = f"{base_path}/{filename}"
|
file_path = f"{base_path}/{filename}"
|
||||||
|
|
||||||
storage.save_file(file, file_path)
|
storage.save_file(file, file_path)
|
||||||
@@ -609,6 +676,8 @@ class UploadFile(Resource):
|
|||||||
job_name,
|
job_name,
|
||||||
filename, # Corrected variable for single-file case
|
filename, # Corrected variable for single-file case
|
||||||
user,
|
user,
|
||||||
|
dir_name,
|
||||||
|
safe_user,
|
||||||
)
|
)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
current_app.logger.error(f"Error uploading file: {err}", exc_info=True)
|
current_app.logger.error(f"Error uploading file: {err}", exc_info=True)
|
||||||
@@ -1042,27 +1111,28 @@ class UpdatePrompt(Resource):
|
|||||||
|
|
||||||
@user_ns.route("/api/get_agent")
|
@user_ns.route("/api/get_agent")
|
||||||
class GetAgent(Resource):
|
class GetAgent(Resource):
|
||||||
@api.doc(params={"id": "ID of the agent"}, description="Get a single agent by ID")
|
@api.doc(params={"id": "Agent ID"}, description="Get agent by ID")
|
||||||
def get(self):
|
def get(self):
|
||||||
decoded_token = request.decoded_token
|
if not (decoded_token := request.decoded_token):
|
||||||
if not decoded_token:
|
return {"success": False}, 401
|
||||||
return make_response(jsonify({"success": False}), 401)
|
|
||||||
user = decoded_token.get("sub")
|
if not (agent_id := request.args.get("id")):
|
||||||
agent_id = request.args.get("id")
|
return {"success": False, "message": "ID required"}, 400
|
||||||
if not agent_id:
|
|
||||||
return make_response(
|
|
||||||
jsonify({"success": False, "message": "ID is required"}), 400
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
agent = agents_collection.find_one(
|
agent = agents_collection.find_one(
|
||||||
{"_id": ObjectId(agent_id), "user": user}
|
{"_id": ObjectId(agent_id), "user": decoded_token["sub"]}
|
||||||
)
|
)
|
||||||
if not agent:
|
if not agent:
|
||||||
return make_response(jsonify({"status": "Not found"}), 404)
|
return {"status": "Not found"}, 404
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"id": str(agent["_id"]),
|
"id": str(agent["_id"]),
|
||||||
"name": agent["name"],
|
"name": agent["name"],
|
||||||
"description": agent.get("description", ""),
|
"description": agent.get("description", ""),
|
||||||
|
"image": (
|
||||||
|
generate_image_url(agent["image"]) if agent.get("image") else ""
|
||||||
|
),
|
||||||
"source": (
|
"source": (
|
||||||
str(source_doc["_id"])
|
str(source_doc["_id"])
|
||||||
if isinstance(agent.get("source"), DBRef)
|
if isinstance(agent.get("source"), DBRef)
|
||||||
@@ -1089,19 +1159,20 @@ class GetAgent(Resource):
|
|||||||
"shared_metadata": agent.get("shared_metadata", {}),
|
"shared_metadata": agent.get("shared_metadata", {}),
|
||||||
"shared_token": agent.get("shared_token", ""),
|
"shared_token": agent.get("shared_token", ""),
|
||||||
}
|
}
|
||||||
except Exception as err:
|
return make_response(jsonify(data), 200)
|
||||||
current_app.logger.error(f"Error retrieving agent: {err}", exc_info=True)
|
|
||||||
return make_response(jsonify({"success": False}), 400)
|
except Exception as e:
|
||||||
return make_response(jsonify(data), 200)
|
current_app.logger.error(f"Agent fetch error: {e}", exc_info=True)
|
||||||
|
return {"success": False}, 400
|
||||||
|
|
||||||
|
|
||||||
@user_ns.route("/api/get_agents")
|
@user_ns.route("/api/get_agents")
|
||||||
class GetAgents(Resource):
|
class GetAgents(Resource):
|
||||||
@api.doc(description="Retrieve agents for the user")
|
@api.doc(description="Retrieve agents for the user")
|
||||||
def get(self):
|
def get(self):
|
||||||
decoded_token = request.decoded_token
|
if not (decoded_token := request.decoded_token):
|
||||||
if not decoded_token:
|
return {"success": False}, 401
|
||||||
return make_response(jsonify({"success": False}), 401)
|
|
||||||
user = decoded_token.get("sub")
|
user = decoded_token.get("sub")
|
||||||
try:
|
try:
|
||||||
user_doc = ensure_user_doc(user)
|
user_doc = ensure_user_doc(user)
|
||||||
@@ -1113,6 +1184,9 @@ class GetAgents(Resource):
|
|||||||
"id": str(agent["_id"]),
|
"id": str(agent["_id"]),
|
||||||
"name": agent["name"],
|
"name": agent["name"],
|
||||||
"description": agent.get("description", ""),
|
"description": agent.get("description", ""),
|
||||||
|
"image": (
|
||||||
|
generate_image_url(agent["image"]) if agent.get("image") else ""
|
||||||
|
),
|
||||||
"source": (
|
"source": (
|
||||||
str(source_doc["_id"])
|
str(source_doc["_id"])
|
||||||
if isinstance(agent.get("source"), DBRef)
|
if isinstance(agent.get("source"), DBRef)
|
||||||
@@ -1157,8 +1231,8 @@ class CreateAgent(Resource):
|
|||||||
"description": fields.String(
|
"description": fields.String(
|
||||||
required=True, description="Description of the agent"
|
required=True, description="Description of the agent"
|
||||||
),
|
),
|
||||||
"image": fields.String(
|
"image": fields.Raw(
|
||||||
required=False, description="Image URL or identifier"
|
required=False, description="Image file upload", type="file"
|
||||||
),
|
),
|
||||||
"source": fields.String(required=True, description="Source ID"),
|
"source": fields.String(required=True, description="Source ID"),
|
||||||
"chunks": fields.Integer(required=True, description="Chunks count"),
|
"chunks": fields.Integer(required=True, description="Chunks count"),
|
||||||
@@ -1177,12 +1251,20 @@ class CreateAgent(Resource):
|
|||||||
@api.expect(create_agent_model)
|
@api.expect(create_agent_model)
|
||||||
@api.doc(description="Create a new agent")
|
@api.doc(description="Create a new agent")
|
||||||
def post(self):
|
def post(self):
|
||||||
decoded_token = request.decoded_token
|
if not (decoded_token := request.decoded_token):
|
||||||
if not decoded_token:
|
return {"success": False}, 401
|
||||||
return make_response(jsonify({"success": False}), 401)
|
|
||||||
user = decoded_token.get("sub")
|
user = decoded_token.get("sub")
|
||||||
data = request.get_json()
|
if request.content_type == "application/json":
|
||||||
|
data = request.get_json()
|
||||||
|
else:
|
||||||
|
print(request.form)
|
||||||
|
data = request.form.to_dict()
|
||||||
|
if "tools" in data:
|
||||||
|
try:
|
||||||
|
data["tools"] = json.loads(data["tools"])
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
data["tools"] = []
|
||||||
|
print(f"Received data: {data}")
|
||||||
if data.get("status") not in ["draft", "published"]:
|
if data.get("status") not in ["draft", "published"]:
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({"success": False, "message": "Invalid status"}), 400
|
jsonify({"success": False, "message": "Invalid status"}), 400
|
||||||
@@ -1203,13 +1285,20 @@ class CreateAgent(Resource):
|
|||||||
missing_fields = check_required_fields(data, required_fields)
|
missing_fields = check_required_fields(data, required_fields)
|
||||||
if missing_fields:
|
if missing_fields:
|
||||||
return missing_fields
|
return missing_fields
|
||||||
|
|
||||||
|
image_url, error = handle_image_upload(request, "", user, storage)
|
||||||
|
if error:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Image upload failed"}), 400
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
key = str(uuid.uuid4())
|
key = str(uuid.uuid4())
|
||||||
new_agent = {
|
new_agent = {
|
||||||
"user": user,
|
"user": user,
|
||||||
"name": data.get("name"),
|
"name": data.get("name"),
|
||||||
"description": data.get("description", ""),
|
"description": data.get("description", ""),
|
||||||
"image": data.get("image", ""),
|
"image": image_url,
|
||||||
"source": (
|
"source": (
|
||||||
DBRef("sources", ObjectId(data.get("source")))
|
DBRef("sources", ObjectId(data.get("source")))
|
||||||
if ObjectId.is_valid(data.get("source"))
|
if ObjectId.is_valid(data.get("source"))
|
||||||
@@ -1267,11 +1356,18 @@ class UpdateAgent(Resource):
|
|||||||
@api.expect(update_agent_model)
|
@api.expect(update_agent_model)
|
||||||
@api.doc(description="Update an existing agent")
|
@api.doc(description="Update an existing agent")
|
||||||
def put(self, agent_id):
|
def put(self, agent_id):
|
||||||
decoded_token = request.decoded_token
|
if not (decoded_token := request.decoded_token):
|
||||||
if not decoded_token:
|
return {"success": False}, 401
|
||||||
return make_response(jsonify({"success": False}), 401)
|
|
||||||
user = decoded_token.get("sub")
|
user = decoded_token.get("sub")
|
||||||
data = request.get_json()
|
if request.content_type == "application/json":
|
||||||
|
data = request.get_json()
|
||||||
|
else:
|
||||||
|
data = request.form.to_dict()
|
||||||
|
if "tools" in data:
|
||||||
|
try:
|
||||||
|
data["tools"] = json.loads(data["tools"])
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
data["tools"] = []
|
||||||
|
|
||||||
if not ObjectId.is_valid(agent_id):
|
if not ObjectId.is_valid(agent_id):
|
||||||
return make_response(
|
return make_response(
|
||||||
@@ -1296,6 +1392,15 @@ class UpdateAgent(Resource):
|
|||||||
),
|
),
|
||||||
404,
|
404,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
image_url, error = handle_image_upload(
|
||||||
|
request, existing_agent.get("image", ""), user, storage
|
||||||
|
)
|
||||||
|
if error:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Image upload failed"}), 400
|
||||||
|
)
|
||||||
|
|
||||||
update_fields = {}
|
update_fields = {}
|
||||||
allowed_fields = [
|
allowed_fields = [
|
||||||
"name",
|
"name",
|
||||||
@@ -1367,6 +1472,8 @@ class UpdateAgent(Resource):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
update_fields[field] = data[field]
|
update_fields[field] = data[field]
|
||||||
|
if image_url:
|
||||||
|
update_fields["image"] = image_url
|
||||||
if not update_fields:
|
if not update_fields:
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({"success": False, "message": "No update data provided"}), 400
|
jsonify({"success": False, "message": "No update data provided"}), 400
|
||||||
@@ -1515,6 +1622,9 @@ class PinnedAgents(Resource):
|
|||||||
"id": str(agent["_id"]),
|
"id": str(agent["_id"]),
|
||||||
"name": agent.get("name", ""),
|
"name": agent.get("name", ""),
|
||||||
"description": agent.get("description", ""),
|
"description": agent.get("description", ""),
|
||||||
|
"image": (
|
||||||
|
generate_image_url(agent["image"]) if agent.get("image") else ""
|
||||||
|
),
|
||||||
"source": (
|
"source": (
|
||||||
str(db.dereference(agent["source"])["_id"])
|
str(db.dereference(agent["source"])["_id"])
|
||||||
if "source" in agent
|
if "source" in agent
|
||||||
@@ -1675,6 +1785,11 @@ class SharedAgent(Resource):
|
|||||||
"id": agent_id,
|
"id": agent_id,
|
||||||
"user": shared_agent.get("user", ""),
|
"user": shared_agent.get("user", ""),
|
||||||
"name": shared_agent.get("name", ""),
|
"name": shared_agent.get("name", ""),
|
||||||
|
"image": (
|
||||||
|
generate_image_url(shared_agent["image"])
|
||||||
|
if shared_agent.get("image")
|
||||||
|
else ""
|
||||||
|
),
|
||||||
"description": shared_agent.get("description", ""),
|
"description": shared_agent.get("description", ""),
|
||||||
"tools": shared_agent.get("tools", []),
|
"tools": shared_agent.get("tools", []),
|
||||||
"tool_details": resolve_tool_details(shared_agent.get("tools", [])),
|
"tool_details": resolve_tool_details(shared_agent.get("tools", [])),
|
||||||
@@ -1750,6 +1865,9 @@ class SharedAgents(Resource):
|
|||||||
"id": str(agent["_id"]),
|
"id": str(agent["_id"]),
|
||||||
"name": agent.get("name", ""),
|
"name": agent.get("name", ""),
|
||||||
"description": agent.get("description", ""),
|
"description": agent.get("description", ""),
|
||||||
|
"image": (
|
||||||
|
generate_image_url(agent["image"]) if agent.get("image") else ""
|
||||||
|
),
|
||||||
"tools": agent.get("tools", []),
|
"tools": agent.get("tools", []),
|
||||||
"tool_details": resolve_tool_details(agent.get("tools", [])),
|
"tool_details": resolve_tool_details(agent.get("tools", [])),
|
||||||
"agent_type": agent.get("agent_type", ""),
|
"agent_type": agent.get("agent_type", ""),
|
||||||
@@ -2205,7 +2323,7 @@ class GetPubliclySharedConversations(Resource):
|
|||||||
return make_response(
|
return make_response(
|
||||||
jsonify(
|
jsonify(
|
||||||
{
|
{
|
||||||
"sucess": False,
|
"success": False,
|
||||||
"error": "might have broken url or the conversation does not exist",
|
"error": "might have broken url or the conversation does not exist",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -2214,11 +2332,35 @@ class GetPubliclySharedConversations(Resource):
|
|||||||
conversation_queries = conversation["queries"][
|
conversation_queries = conversation["queries"][
|
||||||
: (shared["first_n_queries"])
|
: (shared["first_n_queries"])
|
||||||
]
|
]
|
||||||
|
|
||||||
|
for query in conversation_queries:
|
||||||
|
if "attachments" in query and query["attachments"]:
|
||||||
|
attachment_details = []
|
||||||
|
for attachment_id in query["attachments"]:
|
||||||
|
try:
|
||||||
|
attachment = attachments_collection.find_one(
|
||||||
|
{"_id": ObjectId(attachment_id)}
|
||||||
|
)
|
||||||
|
if attachment:
|
||||||
|
attachment_details.append(
|
||||||
|
{
|
||||||
|
"id": str(attachment["_id"]),
|
||||||
|
"fileName": attachment.get(
|
||||||
|
"filename", "Unknown file"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(
|
||||||
|
f"Error retrieving attachment {attachment_id}: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
query["attachments"] = attachment_details
|
||||||
else:
|
else:
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify(
|
jsonify(
|
||||||
{
|
{
|
||||||
"sucess": False,
|
"success": False,
|
||||||
"error": "might have broken url or the conversation does not exist",
|
"error": "might have broken url or the conversation does not exist",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -3416,7 +3558,7 @@ class StoreAttachment(Resource):
|
|||||||
jsonify({"status": "error", "message": "Missing file"}),
|
jsonify({"status": "error", "message": "Missing file"}),
|
||||||
400,
|
400,
|
||||||
)
|
)
|
||||||
user = secure_filename(decoded_token.get("sub"))
|
user = safe_filename(decoded_token.get("sub"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
attachment_id = ObjectId()
|
attachment_id = ObjectId()
|
||||||
@@ -3447,3 +3589,30 @@ class StoreAttachment(Resource):
|
|||||||
except Exception as err:
|
except Exception as err:
|
||||||
current_app.logger.error(f"Error storing attachment: {err}", exc_info=True)
|
current_app.logger.error(f"Error storing attachment: {err}", exc_info=True)
|
||||||
return make_response(jsonify({"success": False, "error": str(err)}), 400)
|
return make_response(jsonify({"success": False, "error": str(err)}), 400)
|
||||||
|
|
||||||
|
|
||||||
|
@user_ns.route("/api/images/<path:image_path>")
|
||||||
|
class ServeImage(Resource):
|
||||||
|
@api.doc(description="Serve an image from storage")
|
||||||
|
def get(self, image_path):
|
||||||
|
try:
|
||||||
|
file_obj = storage.get_file(image_path)
|
||||||
|
extension = image_path.split(".")[-1].lower()
|
||||||
|
content_type = f"image/{extension}"
|
||||||
|
if extension == "jpg":
|
||||||
|
content_type = "image/jpeg"
|
||||||
|
|
||||||
|
response = make_response(file_obj.read())
|
||||||
|
response.headers.set("Content-Type", content_type)
|
||||||
|
response.headers.set("Cache-Control", "max-age=86400")
|
||||||
|
|
||||||
|
return response
|
||||||
|
except FileNotFoundError:
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Image not found"}), 404
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Error serving image: {e}")
|
||||||
|
return make_response(
|
||||||
|
jsonify({"success": False, "message": "Error retrieving image"}), 500
|
||||||
|
)
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ from application.worker import (
|
|||||||
|
|
||||||
|
|
||||||
@celery.task(bind=True)
|
@celery.task(bind=True)
|
||||||
def ingest(self, directory, formats, name_job, filename, user):
|
def ingest(self, directory, formats, job_name, filename, user, dir_name, user_dir):
|
||||||
resp = ingest_worker(self, directory, formats, name_job, filename, user)
|
resp = ingest_worker(self, directory, formats, job_name, filename, user, dir_name, user_dir)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,18 +11,18 @@ current_dir = os.path.dirname(
|
|||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
AUTH_TYPE: Optional[str] = None
|
AUTH_TYPE: Optional[str] = None
|
||||||
LLM_NAME: str = "docsgpt"
|
LLM_PROVIDER: str = "docsgpt"
|
||||||
MODEL_NAME: Optional[str] = (
|
LLM_NAME: Optional[str] = (
|
||||||
None # if LLM_NAME is openai, MODEL_NAME can be gpt-4 or gpt-3.5-turbo
|
None # if LLM_PROVIDER is openai, LLM_NAME can be gpt-4 or gpt-3.5-turbo
|
||||||
)
|
)
|
||||||
EMBEDDINGS_NAME: str = "huggingface_sentence-transformers/all-mpnet-base-v2"
|
EMBEDDINGS_NAME: str = "huggingface_sentence-transformers/all-mpnet-base-v2"
|
||||||
CELERY_BROKER_URL: str = "redis://localhost:6379/0"
|
CELERY_BROKER_URL: str = "redis://localhost:6379/0"
|
||||||
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/1"
|
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/1"
|
||||||
MONGO_URI: str = "mongodb://localhost:27017/docsgpt"
|
MONGO_URI: str = "mongodb://localhost:27017/docsgpt"
|
||||||
MONGO_DB_NAME: str = "docsgpt"
|
MONGO_DB_NAME: str = "docsgpt"
|
||||||
MODEL_PATH: str = os.path.join(current_dir, "models/docsgpt-7b-f16.gguf")
|
LLM_PATH: str = os.path.join(current_dir, "models/docsgpt-7b-f16.gguf")
|
||||||
DEFAULT_MAX_HISTORY: int = 150
|
DEFAULT_MAX_HISTORY: int = 150
|
||||||
MODEL_TOKEN_LIMITS: dict = {
|
LLM_TOKEN_LIMITS: dict = {
|
||||||
"gpt-4o-mini": 128000,
|
"gpt-4o-mini": 128000,
|
||||||
"gpt-3.5-turbo": 4096,
|
"gpt-3.5-turbo": 4096,
|
||||||
"claude-2": 1e5,
|
"claude-2": 1e5,
|
||||||
@@ -35,6 +35,9 @@ class Settings(BaseSettings):
|
|||||||
)
|
)
|
||||||
RETRIEVERS_ENABLED: list = ["classic_rag", "duckduck_search"] # also brave_search
|
RETRIEVERS_ENABLED: list = ["classic_rag", "duckduck_search"] # also brave_search
|
||||||
AGENT_NAME: str = "classic"
|
AGENT_NAME: str = "classic"
|
||||||
|
FALLBACK_LLM_PROVIDER: Optional[str] = None # provider for fallback llm
|
||||||
|
FALLBACK_LLM_NAME: Optional[str] = None # model name for fallback llm
|
||||||
|
FALLBACK_LLM_API_KEY: Optional[str] = None # api key for fallback llm
|
||||||
|
|
||||||
# LLM Cache
|
# LLM Cache
|
||||||
CACHE_REDIS_URL: str = "redis://localhost:6379/2"
|
CACHE_REDIS_URL: str = "redis://localhost:6379/2"
|
||||||
@@ -99,8 +102,8 @@ class Settings(BaseSettings):
|
|||||||
BRAVE_SEARCH_API_KEY: Optional[str] = None
|
BRAVE_SEARCH_API_KEY: Optional[str] = None
|
||||||
|
|
||||||
FLASK_DEBUG_MODE: bool = False
|
FLASK_DEBUG_MODE: bool = False
|
||||||
STORAGE_TYPE: str = "local" # local or s3
|
STORAGE_TYPE: str = "local" # local or s3
|
||||||
|
URL_STRATEGY: str = "backend" # backend or s3
|
||||||
|
|
||||||
JWT_SECRET_KEY: str = ""
|
JWT_SECRET_KEY: str = ""
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +1,117 @@
|
|||||||
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
from application.cache import gen_cache, stream_cache
|
from application.cache import gen_cache, stream_cache
|
||||||
|
|
||||||
|
from application.core.settings import settings
|
||||||
from application.usage import gen_token_usage, stream_token_usage
|
from application.usage import gen_token_usage, stream_token_usage
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class BaseLLM(ABC):
|
class BaseLLM(ABC):
|
||||||
def __init__(self, decoded_token=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
decoded_token=None,
|
||||||
|
):
|
||||||
self.decoded_token = decoded_token
|
self.decoded_token = decoded_token
|
||||||
self.token_usage = {"prompt_tokens": 0, "generated_tokens": 0}
|
self.token_usage = {"prompt_tokens": 0, "generated_tokens": 0}
|
||||||
|
self.fallback_provider = settings.FALLBACK_LLM_PROVIDER
|
||||||
|
self.fallback_model_name = settings.FALLBACK_LLM_NAME
|
||||||
|
self.fallback_llm_api_key = settings.FALLBACK_LLM_API_KEY
|
||||||
|
self._fallback_llm = None
|
||||||
|
|
||||||
def _apply_decorator(self, method, decorators, *args, **kwargs):
|
@property
|
||||||
for decorator in decorators:
|
def fallback_llm(self):
|
||||||
method = decorator(method)
|
"""Lazy-loaded fallback LLM instance."""
|
||||||
return method(self, *args, **kwargs)
|
if (
|
||||||
|
self._fallback_llm is None
|
||||||
|
and self.fallback_provider
|
||||||
|
and self.fallback_model_name
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
from application.llm.llm_creator import LLMCreator
|
||||||
|
|
||||||
|
self._fallback_llm = LLMCreator.create_llm(
|
||||||
|
self.fallback_provider,
|
||||||
|
self.fallback_llm_api_key,
|
||||||
|
None,
|
||||||
|
self.decoded_token,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Failed to initialize fallback LLM: {str(e)}", exc_info=True
|
||||||
|
)
|
||||||
|
return self._fallback_llm
|
||||||
|
|
||||||
|
def _execute_with_fallback(
|
||||||
|
self, method_name: str, decorators: list, *args, **kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Unified method execution with fallback support.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method_name: Name of the raw method ('_raw_gen' or '_raw_gen_stream')
|
||||||
|
decorators: List of decorators to apply
|
||||||
|
*args: Positional arguments
|
||||||
|
**kwargs: Keyword arguments
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorated_method():
|
||||||
|
method = getattr(self, method_name)
|
||||||
|
for decorator in decorators:
|
||||||
|
method = decorator(method)
|
||||||
|
return method(self, *args, **kwargs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return decorated_method()
|
||||||
|
except Exception as e:
|
||||||
|
if not self.fallback_llm:
|
||||||
|
logger.error(f"Primary LLM failed and no fallback available: {str(e)}")
|
||||||
|
raise
|
||||||
|
logger.warning(
|
||||||
|
f"Falling back to {self.fallback_provider}/{self.fallback_model_name}. Error: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
fallback_method = getattr(
|
||||||
|
self.fallback_llm, method_name.replace("_raw_", "")
|
||||||
|
)
|
||||||
|
return fallback_method(*args, **kwargs)
|
||||||
|
|
||||||
|
def gen(self, model, messages, stream=False, tools=None, *args, **kwargs):
|
||||||
|
decorators = [gen_token_usage, gen_cache]
|
||||||
|
return self._execute_with_fallback(
|
||||||
|
"_raw_gen",
|
||||||
|
decorators,
|
||||||
|
model=model,
|
||||||
|
messages=messages,
|
||||||
|
stream=stream,
|
||||||
|
tools=tools,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
def gen_stream(self, model, messages, stream=True, tools=None, *args, **kwargs):
|
||||||
|
decorators = [stream_cache, stream_token_usage]
|
||||||
|
return self._execute_with_fallback(
|
||||||
|
"_raw_gen_stream",
|
||||||
|
decorators,
|
||||||
|
model=model,
|
||||||
|
messages=messages,
|
||||||
|
stream=stream,
|
||||||
|
tools=tools,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _raw_gen(self, model, messages, stream, tools, *args, **kwargs):
|
def _raw_gen(self, model, messages, stream, tools, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def gen(self, model, messages, stream=False, tools=None, *args, **kwargs):
|
|
||||||
decorators = [gen_token_usage, gen_cache]
|
|
||||||
return self._apply_decorator(
|
|
||||||
self._raw_gen,
|
|
||||||
decorators=decorators,
|
|
||||||
model=model,
|
|
||||||
messages=messages,
|
|
||||||
stream=stream,
|
|
||||||
tools=tools,
|
|
||||||
*args,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _raw_gen_stream(self, model, messages, stream, *args, **kwargs):
|
def _raw_gen_stream(self, model, messages, stream, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def gen_stream(self, model, messages, stream=True, tools=None, *args, **kwargs):
|
|
||||||
decorators = [stream_cache, stream_token_usage]
|
|
||||||
return self._apply_decorator(
|
|
||||||
self._raw_gen_stream,
|
|
||||||
decorators=decorators,
|
|
||||||
model=model,
|
|
||||||
messages=messages,
|
|
||||||
stream=stream,
|
|
||||||
tools=tools,
|
|
||||||
*args,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
def supports_tools(self):
|
def supports_tools(self):
|
||||||
return hasattr(self, "_supports_tools") and callable(
|
return hasattr(self, "_supports_tools") and callable(
|
||||||
getattr(self, "_supports_tools")
|
getattr(self, "_supports_tools")
|
||||||
@@ -55,11 +119,11 @@ class BaseLLM(ABC):
|
|||||||
|
|
||||||
def _supports_tools(self):
|
def _supports_tools(self):
|
||||||
raise NotImplementedError("Subclass must implement _supports_tools method")
|
raise NotImplementedError("Subclass must implement _supports_tools method")
|
||||||
|
|
||||||
def get_supported_attachment_types(self):
|
def get_supported_attachment_types(self):
|
||||||
"""
|
"""
|
||||||
Return a list of MIME types supported by this LLM for file uploads.
|
Return a list of MIME types supported by this LLM for file uploads.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: List of supported MIME types
|
list: List of supported MIME types
|
||||||
"""
|
"""
|
||||||
|
|||||||
0
application/llm/handlers/__init__.py
Normal file
335
application/llm/handlers/base.py
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
import logging
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, Generator, List, Optional, Union
|
||||||
|
|
||||||
|
from application.logging import build_stack_data
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ToolCall:
|
||||||
|
"""Represents a tool/function call from the LLM."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
arguments: Union[str, Dict]
|
||||||
|
index: Optional[int] = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Dict) -> "ToolCall":
|
||||||
|
"""Create ToolCall from dictionary."""
|
||||||
|
return cls(
|
||||||
|
id=data.get("id", ""),
|
||||||
|
name=data.get("name", ""),
|
||||||
|
arguments=data.get("arguments", {}),
|
||||||
|
index=data.get("index"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LLMResponse:
|
||||||
|
"""Represents a response from the LLM."""
|
||||||
|
|
||||||
|
content: str
|
||||||
|
tool_calls: List[ToolCall]
|
||||||
|
finish_reason: str
|
||||||
|
raw_response: Any
|
||||||
|
|
||||||
|
@property
|
||||||
|
def requires_tool_call(self) -> bool:
|
||||||
|
"""Check if the response requires tool calls."""
|
||||||
|
return bool(self.tool_calls) and self.finish_reason == "tool_calls"
|
||||||
|
|
||||||
|
|
||||||
|
class LLMHandler(ABC):
|
||||||
|
"""Abstract base class for LLM handlers."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.llm_calls = []
|
||||||
|
self.tool_calls = []
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def parse_response(self, response: Any) -> LLMResponse:
|
||||||
|
"""Parse raw LLM response into standardized format."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def create_tool_message(self, tool_call: ToolCall, result: Any) -> Dict:
|
||||||
|
"""Create a tool result message for the conversation history."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _iterate_stream(self, response: Any) -> Generator:
|
||||||
|
"""Iterate through streaming response chunks."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def process_message_flow(
|
||||||
|
self,
|
||||||
|
agent,
|
||||||
|
initial_response,
|
||||||
|
tools_dict: Dict,
|
||||||
|
messages: List[Dict],
|
||||||
|
attachments: Optional[List] = None,
|
||||||
|
stream: bool = False,
|
||||||
|
) -> Union[str, Generator]:
|
||||||
|
"""
|
||||||
|
Main orchestration method for processing LLM message flow.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
agent: The agent instance
|
||||||
|
initial_response: Initial LLM response
|
||||||
|
tools_dict: Dictionary of available tools
|
||||||
|
messages: Conversation history
|
||||||
|
attachments: Optional attachments
|
||||||
|
stream: Whether to use streaming
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Final response or generator for streaming
|
||||||
|
"""
|
||||||
|
messages = self.prepare_messages(agent, messages, attachments)
|
||||||
|
|
||||||
|
if stream:
|
||||||
|
return self.handle_streaming(agent, initial_response, tools_dict, messages)
|
||||||
|
else:
|
||||||
|
return self.handle_non_streaming(
|
||||||
|
agent, initial_response, tools_dict, messages
|
||||||
|
)
|
||||||
|
|
||||||
|
def prepare_messages(
|
||||||
|
self, agent, messages: List[Dict], attachments: Optional[List] = None
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Prepare messages with attachments and provider-specific formatting.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
agent: The agent instance
|
||||||
|
messages: Original messages
|
||||||
|
attachments: List of attachments
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Prepared messages list
|
||||||
|
"""
|
||||||
|
if not attachments:
|
||||||
|
return messages
|
||||||
|
logger.info(f"Preparing messages with {len(attachments)} attachments")
|
||||||
|
supported_types = agent.llm.get_supported_attachment_types()
|
||||||
|
|
||||||
|
supported_attachments = [
|
||||||
|
a for a in attachments if a.get("mime_type") in supported_types
|
||||||
|
]
|
||||||
|
unsupported_attachments = [
|
||||||
|
a for a in attachments if a.get("mime_type") not in supported_types
|
||||||
|
]
|
||||||
|
|
||||||
|
# Process supported attachments with the LLM's custom method
|
||||||
|
|
||||||
|
if supported_attachments:
|
||||||
|
logger.info(
|
||||||
|
f"Processing {len(supported_attachments)} supported attachments"
|
||||||
|
)
|
||||||
|
messages = agent.llm.prepare_messages_with_attachments(
|
||||||
|
messages, supported_attachments
|
||||||
|
)
|
||||||
|
# Process unsupported attachments with default method
|
||||||
|
|
||||||
|
if unsupported_attachments:
|
||||||
|
logger.info(
|
||||||
|
f"Processing {len(unsupported_attachments)} unsupported attachments"
|
||||||
|
)
|
||||||
|
messages = self._append_unsupported_attachments(
|
||||||
|
messages, unsupported_attachments
|
||||||
|
)
|
||||||
|
return messages
|
||||||
|
|
||||||
|
def _append_unsupported_attachments(
|
||||||
|
self, messages: List[Dict], attachments: List[Dict]
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Default method to append unsupported attachment content to system prompt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages: Current messages
|
||||||
|
attachments: List of unsupported attachments
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Updated messages list
|
||||||
|
"""
|
||||||
|
prepared_messages = messages.copy()
|
||||||
|
attachment_texts = []
|
||||||
|
|
||||||
|
for attachment in attachments:
|
||||||
|
logger.info(f"Adding attachment {attachment.get('id')} to context")
|
||||||
|
if "content" in attachment:
|
||||||
|
attachment_texts.append(
|
||||||
|
f"Attached file content:\n\n{attachment['content']}"
|
||||||
|
)
|
||||||
|
if attachment_texts:
|
||||||
|
combined_text = "\n\n".join(attachment_texts)
|
||||||
|
|
||||||
|
system_msg = next(
|
||||||
|
(msg for msg in prepared_messages if msg.get("role") == "system"),
|
||||||
|
{"role": "system", "content": ""},
|
||||||
|
)
|
||||||
|
|
||||||
|
if system_msg not in prepared_messages:
|
||||||
|
prepared_messages.insert(0, system_msg)
|
||||||
|
system_msg["content"] += f"\n\n{combined_text}"
|
||||||
|
return prepared_messages
|
||||||
|
|
||||||
|
def handle_tool_calls(
|
||||||
|
self, agent, tool_calls: List[ToolCall], tools_dict: Dict, messages: List[Dict]
|
||||||
|
) -> Generator:
|
||||||
|
"""
|
||||||
|
Execute tool calls and update conversation history.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
agent: The agent instance
|
||||||
|
tool_calls: List of tool calls to execute
|
||||||
|
tools_dict: Available tools dictionary
|
||||||
|
messages: Current conversation history
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Updated messages list
|
||||||
|
"""
|
||||||
|
updated_messages = messages.copy()
|
||||||
|
|
||||||
|
for call in tool_calls:
|
||||||
|
try:
|
||||||
|
self.tool_calls.append(call)
|
||||||
|
tool_executor_gen = agent._execute_tool_action(tools_dict, call)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
yield next(tool_executor_gen)
|
||||||
|
except StopIteration as e:
|
||||||
|
tool_response, call_id = e.value
|
||||||
|
break
|
||||||
|
|
||||||
|
updated_messages.append(
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"function_call": {
|
||||||
|
"name": call.name,
|
||||||
|
"args": call.arguments,
|
||||||
|
"call_id": call_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_messages.append(self.create_tool_message(call, tool_response))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error executing tool: {str(e)}", exc_info=True)
|
||||||
|
updated_messages.append(
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"content": f"Error executing tool: {str(e)}",
|
||||||
|
"tool_call_id": call.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return updated_messages
|
||||||
|
|
||||||
|
def handle_non_streaming(
|
||||||
|
self, agent, response: Any, tools_dict: Dict, messages: List[Dict]
|
||||||
|
) -> Generator:
|
||||||
|
"""
|
||||||
|
Handle non-streaming response flow.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
agent: The agent instance
|
||||||
|
response: Current LLM response
|
||||||
|
tools_dict: Available tools dictionary
|
||||||
|
messages: Conversation history
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Final response after processing all tool calls
|
||||||
|
"""
|
||||||
|
parsed = self.parse_response(response)
|
||||||
|
self.llm_calls.append(build_stack_data(agent.llm))
|
||||||
|
|
||||||
|
while parsed.requires_tool_call:
|
||||||
|
tool_handler_gen = self.handle_tool_calls(
|
||||||
|
agent, parsed.tool_calls, tools_dict, messages
|
||||||
|
)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
yield next(tool_handler_gen)
|
||||||
|
except StopIteration as e:
|
||||||
|
messages = e.value
|
||||||
|
break
|
||||||
|
|
||||||
|
response = agent.llm.gen(
|
||||||
|
model=agent.gpt_model, messages=messages, tools=agent.tools
|
||||||
|
)
|
||||||
|
parsed = self.parse_response(response)
|
||||||
|
self.llm_calls.append(build_stack_data(agent.llm))
|
||||||
|
|
||||||
|
return parsed.content
|
||||||
|
|
||||||
|
def handle_streaming(
|
||||||
|
self, agent, response: Any, tools_dict: Dict, messages: List[Dict]
|
||||||
|
) -> Generator:
|
||||||
|
"""
|
||||||
|
Handle streaming response flow.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
agent: The agent instance
|
||||||
|
response: Current LLM response
|
||||||
|
tools_dict: Available tools dictionary
|
||||||
|
messages: Conversation history
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
Streaming response chunks
|
||||||
|
"""
|
||||||
|
buffer = ""
|
||||||
|
tool_calls = {}
|
||||||
|
|
||||||
|
for chunk in self._iterate_stream(response):
|
||||||
|
if isinstance(chunk, str):
|
||||||
|
yield chunk
|
||||||
|
continue
|
||||||
|
parsed = self.parse_response(chunk)
|
||||||
|
|
||||||
|
if parsed.tool_calls:
|
||||||
|
for call in parsed.tool_calls:
|
||||||
|
if call.index not in tool_calls:
|
||||||
|
tool_calls[call.index] = call
|
||||||
|
else:
|
||||||
|
existing = tool_calls[call.index]
|
||||||
|
if call.id:
|
||||||
|
existing.id = call.id
|
||||||
|
if call.name:
|
||||||
|
existing.name = call.name
|
||||||
|
if call.arguments:
|
||||||
|
existing.arguments += call.arguments
|
||||||
|
if parsed.finish_reason == "tool_calls":
|
||||||
|
tool_handler_gen = self.handle_tool_calls(
|
||||||
|
agent, list(tool_calls.values()), tools_dict, messages
|
||||||
|
)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
yield next(tool_handler_gen)
|
||||||
|
except StopIteration as e:
|
||||||
|
messages = e.value
|
||||||
|
break
|
||||||
|
tool_calls = {}
|
||||||
|
|
||||||
|
response = agent.llm.gen_stream(
|
||||||
|
model=agent.gpt_model, messages=messages, tools=agent.tools
|
||||||
|
)
|
||||||
|
self.llm_calls.append(build_stack_data(agent.llm))
|
||||||
|
|
||||||
|
yield from self.handle_streaming(agent, response, tools_dict, messages)
|
||||||
|
return
|
||||||
|
if parsed.content:
|
||||||
|
buffer += parsed.content
|
||||||
|
yield buffer
|
||||||
|
buffer = ""
|
||||||
|
if parsed.finish_reason == "stop":
|
||||||
|
return
|
||||||
78
application/llm/handlers/google.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import Any, Dict, Generator
|
||||||
|
|
||||||
|
from application.llm.handlers.base import LLMHandler, LLMResponse, ToolCall
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleLLMHandler(LLMHandler):
|
||||||
|
"""Handler for Google's GenAI API."""
|
||||||
|
|
||||||
|
def parse_response(self, response: Any) -> LLMResponse:
|
||||||
|
"""Parse Google response into standardized format."""
|
||||||
|
|
||||||
|
if isinstance(response, str):
|
||||||
|
return LLMResponse(
|
||||||
|
content=response,
|
||||||
|
tool_calls=[],
|
||||||
|
finish_reason="stop",
|
||||||
|
raw_response=response,
|
||||||
|
)
|
||||||
|
|
||||||
|
if hasattr(response, "candidates"):
|
||||||
|
parts = response.candidates[0].content.parts if response.candidates else []
|
||||||
|
tool_calls = [
|
||||||
|
ToolCall(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
name=part.function_call.name,
|
||||||
|
arguments=part.function_call.args,
|
||||||
|
)
|
||||||
|
for part in parts
|
||||||
|
if hasattr(part, "function_call") and part.function_call is not None
|
||||||
|
]
|
||||||
|
|
||||||
|
content = " ".join(
|
||||||
|
part.text
|
||||||
|
for part in parts
|
||||||
|
if hasattr(part, "text") and part.text is not None
|
||||||
|
)
|
||||||
|
return LLMResponse(
|
||||||
|
content=content,
|
||||||
|
tool_calls=tool_calls,
|
||||||
|
finish_reason="tool_calls" if tool_calls else "stop",
|
||||||
|
raw_response=response,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
tool_calls = []
|
||||||
|
if hasattr(response, "function_call"):
|
||||||
|
tool_calls.append(
|
||||||
|
ToolCall(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
name=response.function_call.name,
|
||||||
|
arguments=response.function_call.args,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return LLMResponse(
|
||||||
|
content=response.text if hasattr(response, "text") else "",
|
||||||
|
tool_calls=tool_calls,
|
||||||
|
finish_reason="tool_calls" if tool_calls else "stop",
|
||||||
|
raw_response=response,
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_tool_message(self, tool_call: ToolCall, result: Any) -> Dict:
|
||||||
|
"""Create Google-style tool message."""
|
||||||
|
from google.genai import types
|
||||||
|
|
||||||
|
return {
|
||||||
|
"role": "tool",
|
||||||
|
"content": [
|
||||||
|
types.Part.from_function_response(
|
||||||
|
name=tool_call.name, response={"result": result}
|
||||||
|
).to_json_dict()
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _iterate_stream(self, response: Any) -> Generator:
|
||||||
|
"""Iterate through Google streaming response."""
|
||||||
|
for chunk in response:
|
||||||
|
yield chunk
|
||||||
18
application/llm/handlers/handler_creator.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from application.llm.handlers.base import LLMHandler
|
||||||
|
from application.llm.handlers.google import GoogleLLMHandler
|
||||||
|
from application.llm.handlers.openai import OpenAILLMHandler
|
||||||
|
|
||||||
|
|
||||||
|
class LLMHandlerCreator:
|
||||||
|
handlers = {
|
||||||
|
"openai": OpenAILLMHandler,
|
||||||
|
"google": GoogleLLMHandler,
|
||||||
|
"default": OpenAILLMHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_handler(cls, llm_type: str, *args, **kwargs) -> LLMHandler:
|
||||||
|
handler_class = cls.handlers.get(llm_type.lower())
|
||||||
|
if not handler_class:
|
||||||
|
raise ValueError(f"No LLM handler class found for type {llm_type}")
|
||||||
|
return handler_class(*args, **kwargs)
|
||||||
57
application/llm/handlers/openai.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
from typing import Any, Dict, Generator
|
||||||
|
|
||||||
|
from application.llm.handlers.base import LLMHandler, LLMResponse, ToolCall
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAILLMHandler(LLMHandler):
|
||||||
|
"""Handler for OpenAI API."""
|
||||||
|
|
||||||
|
def parse_response(self, response: Any) -> LLMResponse:
|
||||||
|
"""Parse OpenAI response into standardized format."""
|
||||||
|
if isinstance(response, str):
|
||||||
|
return LLMResponse(
|
||||||
|
content=response,
|
||||||
|
tool_calls=[],
|
||||||
|
finish_reason="stop",
|
||||||
|
raw_response=response,
|
||||||
|
)
|
||||||
|
|
||||||
|
message = getattr(response, "message", None) or getattr(response, "delta", None)
|
||||||
|
|
||||||
|
tool_calls = []
|
||||||
|
if hasattr(message, "tool_calls"):
|
||||||
|
tool_calls = [
|
||||||
|
ToolCall(
|
||||||
|
id=getattr(tc, "id", ""),
|
||||||
|
name=getattr(tc.function, "name", ""),
|
||||||
|
arguments=getattr(tc.function, "arguments", ""),
|
||||||
|
index=getattr(tc, "index", None),
|
||||||
|
)
|
||||||
|
for tc in message.tool_calls or []
|
||||||
|
]
|
||||||
|
return LLMResponse(
|
||||||
|
content=getattr(message, "content", ""),
|
||||||
|
tool_calls=tool_calls,
|
||||||
|
finish_reason=getattr(response, "finish_reason", ""),
|
||||||
|
raw_response=response,
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_tool_message(self, tool_call: ToolCall, result: Any) -> Dict:
|
||||||
|
"""Create OpenAI-style tool message."""
|
||||||
|
return {
|
||||||
|
"role": "tool",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"function_response": {
|
||||||
|
"name": tool_call.name,
|
||||||
|
"response": {"result": result},
|
||||||
|
"call_id": tool_call.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _iterate_stream(self, response: Any) -> Generator:
|
||||||
|
"""Iterate through OpenAI streaming response."""
|
||||||
|
for chunk in response:
|
||||||
|
yield chunk
|
||||||
@@ -2,6 +2,7 @@ from application.llm.base import BaseLLM
|
|||||||
from application.core.settings import settings
|
from application.core.settings import settings
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
|
||||||
class LlamaSingleton:
|
class LlamaSingleton:
|
||||||
_instances = {}
|
_instances = {}
|
||||||
_lock = threading.Lock() # Add a lock for thread synchronization
|
_lock = threading.Lock() # Add a lock for thread synchronization
|
||||||
@@ -29,7 +30,7 @@ class LlamaCpp(BaseLLM):
|
|||||||
self,
|
self,
|
||||||
api_key=None,
|
api_key=None,
|
||||||
user_api_key=None,
|
user_api_key=None,
|
||||||
llm_name=settings.MODEL_PATH,
|
llm_name=settings.LLM_PATH,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -42,14 +43,18 @@ class LlamaCpp(BaseLLM):
|
|||||||
context = messages[0]["content"]
|
context = messages[0]["content"]
|
||||||
user_question = messages[-1]["content"]
|
user_question = messages[-1]["content"]
|
||||||
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
|
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
|
||||||
result = LlamaSingleton.query_model(self.llama, prompt, max_tokens=150, echo=False)
|
result = LlamaSingleton.query_model(
|
||||||
|
self.llama, prompt, max_tokens=150, echo=False
|
||||||
|
)
|
||||||
return result["choices"][0]["text"].split("### Answer \n")[-1]
|
return result["choices"][0]["text"].split("### Answer \n")[-1]
|
||||||
|
|
||||||
def _raw_gen_stream(self, baseself, model, messages, stream=True, **kwargs):
|
def _raw_gen_stream(self, baseself, model, messages, stream=True, **kwargs):
|
||||||
context = messages[0]["content"]
|
context = messages[0]["content"]
|
||||||
user_question = messages[-1]["content"]
|
user_question = messages[-1]["content"]
|
||||||
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
|
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
|
||||||
result = LlamaSingleton.query_model(self.llama, prompt, max_tokens=150, echo=False, stream=stream)
|
result = LlamaSingleton.query_model(
|
||||||
|
self.llama, prompt, max_tokens=150, echo=False, stream=stream
|
||||||
|
)
|
||||||
for item in result:
|
for item in result:
|
||||||
for choice in item["choices"]:
|
for choice in item["choices"]:
|
||||||
yield choice["text"]
|
yield choice["text"]
|
||||||
|
|||||||
@@ -64,12 +64,12 @@ python-pptx==1.0.2
|
|||||||
redis==5.2.1
|
redis==5.2.1
|
||||||
referencing>=0.28.0,<0.31.0
|
referencing>=0.28.0,<0.31.0
|
||||||
regex==2024.11.6
|
regex==2024.11.6
|
||||||
requests==2.32.4
|
requests==2.32.3
|
||||||
retry==0.9.2
|
retry==0.9.2
|
||||||
sentence-transformers==3.3.1
|
sentence-transformers==3.3.1
|
||||||
tiktoken==0.8.0
|
tiktoken==0.8.0
|
||||||
tokenizers==0.21.0
|
tokenizers==0.21.0
|
||||||
torch==2.7.1
|
torch==2.7.0
|
||||||
tqdm==4.67.1
|
tqdm==4.67.1
|
||||||
transformers==4.51.3
|
transformers==4.51.3
|
||||||
typing-extensions==4.12.2
|
typing-extensions==4.12.2
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ class BraveRetSearch(BaseRetriever):
|
|||||||
self.token_limit = (
|
self.token_limit = (
|
||||||
token_limit
|
token_limit
|
||||||
if token_limit
|
if token_limit
|
||||||
< settings.MODEL_TOKEN_LIMITS.get(
|
< settings.LLM_TOKEN_LIMITS.get(
|
||||||
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
||||||
)
|
)
|
||||||
else settings.MODEL_TOKEN_LIMITS.get(
|
else settings.LLM_TOKEN_LIMITS.get(
|
||||||
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -59,7 +59,7 @@ class BraveRetSearch(BaseRetriever):
|
|||||||
docs.append({"text": snippet, "title": title, "link": link})
|
docs.append({"text": snippet, "title": title, "link": link})
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
if settings.LLM_NAME == "llama.cpp":
|
if settings.LLM_PROVIDER == "llama.cpp":
|
||||||
docs = [docs[0]]
|
docs = [docs[0]]
|
||||||
|
|
||||||
return docs
|
return docs
|
||||||
@@ -84,7 +84,7 @@ class BraveRetSearch(BaseRetriever):
|
|||||||
messages_combine.append({"role": "user", "content": self.question})
|
messages_combine.append({"role": "user", "content": self.question})
|
||||||
|
|
||||||
llm = LLMCreator.create_llm(
|
llm = LLMCreator.create_llm(
|
||||||
settings.LLM_NAME,
|
settings.LLM_PROVIDER,
|
||||||
api_key=settings.API_KEY,
|
api_key=settings.API_KEY,
|
||||||
user_api_key=self.user_api_key,
|
user_api_key=self.user_api_key,
|
||||||
decoded_token=self.decoded_token,
|
decoded_token=self.decoded_token,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class ClassicRAG(BaseRetriever):
|
|||||||
token_limit=150,
|
token_limit=150,
|
||||||
gpt_model="docsgpt",
|
gpt_model="docsgpt",
|
||||||
user_api_key=None,
|
user_api_key=None,
|
||||||
llm_name=settings.LLM_NAME,
|
llm_name=settings.LLM_PROVIDER,
|
||||||
api_key=settings.API_KEY,
|
api_key=settings.API_KEY,
|
||||||
decoded_token=None,
|
decoded_token=None,
|
||||||
):
|
):
|
||||||
@@ -28,10 +28,10 @@ class ClassicRAG(BaseRetriever):
|
|||||||
self.token_limit = (
|
self.token_limit = (
|
||||||
token_limit
|
token_limit
|
||||||
if token_limit
|
if token_limit
|
||||||
< settings.MODEL_TOKEN_LIMITS.get(
|
< settings.LLM_TOKEN_LIMITS.get(
|
||||||
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
||||||
)
|
)
|
||||||
else settings.MODEL_TOKEN_LIMITS.get(
|
else settings.LLM_TOKEN_LIMITS.get(
|
||||||
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ class DuckDuckSearch(BaseRetriever):
|
|||||||
self.token_limit = (
|
self.token_limit = (
|
||||||
token_limit
|
token_limit
|
||||||
if token_limit
|
if token_limit
|
||||||
< settings.MODEL_TOKEN_LIMITS.get(
|
< settings.LLM_TOKEN_LIMITS.get(
|
||||||
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
||||||
)
|
)
|
||||||
else settings.MODEL_TOKEN_LIMITS.get(
|
else settings.LLM_TOKEN_LIMITS.get(
|
||||||
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -58,7 +58,7 @@ class DuckDuckSearch(BaseRetriever):
|
|||||||
)
|
)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
if settings.LLM_NAME == "llama.cpp":
|
if settings.LLM_PROVIDER == "llama.cpp":
|
||||||
docs = [docs[0]]
|
docs = [docs[0]]
|
||||||
|
|
||||||
return docs
|
return docs
|
||||||
@@ -83,7 +83,7 @@ class DuckDuckSearch(BaseRetriever):
|
|||||||
messages_combine.append({"role": "user", "content": self.question})
|
messages_combine.append({"role": "user", "content": self.question})
|
||||||
|
|
||||||
llm = LLMCreator.create_llm(
|
llm = LLMCreator.create_llm(
|
||||||
settings.LLM_NAME,
|
settings.LLM_PROVIDER,
|
||||||
api_key=settings.API_KEY,
|
api_key=settings.API_KEY,
|
||||||
user_api_key=self.user_api_key,
|
user_api_key=self.user_api_key,
|
||||||
decoded_token=self.decoded_token,
|
decoded_token=self.decoded_token,
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
"""S3 storage implementation."""
|
"""S3 storage implementation."""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
from typing import BinaryIO, List, Callable
|
|
||||||
import os
|
import os
|
||||||
|
from typing import BinaryIO, Callable, List
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
from botocore.exceptions import ClientError
|
from application.core.settings import settings
|
||||||
|
|
||||||
from application.storage.base import BaseStorage
|
from application.storage.base import BaseStorage
|
||||||
from application.core.settings import settings
|
from botocore.exceptions import ClientError
|
||||||
|
|
||||||
|
|
||||||
class S3Storage(BaseStorage):
|
class S3Storage(BaseStorage):
|
||||||
@@ -20,18 +21,21 @@ class S3Storage(BaseStorage):
|
|||||||
Args:
|
Args:
|
||||||
bucket_name: S3 bucket name (optional, defaults to settings)
|
bucket_name: S3 bucket name (optional, defaults to settings)
|
||||||
"""
|
"""
|
||||||
self.bucket_name = bucket_name or getattr(settings, "S3_BUCKET_NAME", "docsgpt-test-bucket")
|
self.bucket_name = bucket_name or getattr(
|
||||||
|
settings, "S3_BUCKET_NAME", "docsgpt-test-bucket"
|
||||||
|
)
|
||||||
|
|
||||||
# Get credentials from settings
|
# Get credentials from settings
|
||||||
|
|
||||||
aws_access_key_id = getattr(settings, "SAGEMAKER_ACCESS_KEY", None)
|
aws_access_key_id = getattr(settings, "SAGEMAKER_ACCESS_KEY", None)
|
||||||
aws_secret_access_key = getattr(settings, "SAGEMAKER_SECRET_KEY", None)
|
aws_secret_access_key = getattr(settings, "SAGEMAKER_SECRET_KEY", None)
|
||||||
region_name = getattr(settings, "SAGEMAKER_REGION", None)
|
region_name = getattr(settings, "SAGEMAKER_REGION", None)
|
||||||
|
|
||||||
self.s3 = boto3.client(
|
self.s3 = boto3.client(
|
||||||
's3',
|
"s3",
|
||||||
aws_access_key_id=aws_access_key_id,
|
aws_access_key_id=aws_access_key_id,
|
||||||
aws_secret_access_key=aws_secret_access_key,
|
aws_secret_access_key=aws_secret_access_key,
|
||||||
region_name=region_name
|
region_name=region_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
def save_file(self, file_data: BinaryIO, path: str) -> dict:
|
def save_file(self, file_data: BinaryIO, path: str) -> dict:
|
||||||
@@ -41,17 +45,16 @@ class S3Storage(BaseStorage):
|
|||||||
region = getattr(settings, "SAGEMAKER_REGION", None)
|
region = getattr(settings, "SAGEMAKER_REGION", None)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'storage_type': 's3',
|
"storage_type": "s3",
|
||||||
'bucket_name': self.bucket_name,
|
"bucket_name": self.bucket_name,
|
||||||
'uri': f's3://{self.bucket_name}/{path}',
|
"uri": f"s3://{self.bucket_name}/{path}",
|
||||||
'region': region
|
"region": region,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_file(self, path: str) -> BinaryIO:
|
def get_file(self, path: str) -> BinaryIO:
|
||||||
"""Get a file from S3 storage."""
|
"""Get a file from S3 storage."""
|
||||||
if not self.file_exists(path):
|
if not self.file_exists(path):
|
||||||
raise FileNotFoundError(f"File not found: {path}")
|
raise FileNotFoundError(f"File not found: {path}")
|
||||||
|
|
||||||
file_obj = io.BytesIO()
|
file_obj = io.BytesIO()
|
||||||
self.s3.download_fileobj(self.bucket_name, path, file_obj)
|
self.s3.download_fileobj(self.bucket_name, path, file_obj)
|
||||||
file_obj.seek(0)
|
file_obj.seek(0)
|
||||||
@@ -76,18 +79,17 @@ class S3Storage(BaseStorage):
|
|||||||
def list_files(self, directory: str) -> List[str]:
|
def list_files(self, directory: str) -> List[str]:
|
||||||
"""List all files in a directory in S3 storage."""
|
"""List all files in a directory in S3 storage."""
|
||||||
# Ensure directory ends with a slash if it's not empty
|
# Ensure directory ends with a slash if it's not empty
|
||||||
if directory and not directory.endswith('/'):
|
|
||||||
directory += '/'
|
|
||||||
|
|
||||||
|
if directory and not directory.endswith("/"):
|
||||||
|
directory += "/"
|
||||||
result = []
|
result = []
|
||||||
paginator = self.s3.get_paginator('list_objects_v2')
|
paginator = self.s3.get_paginator("list_objects_v2")
|
||||||
pages = paginator.paginate(Bucket=self.bucket_name, Prefix=directory)
|
pages = paginator.paginate(Bucket=self.bucket_name, Prefix=directory)
|
||||||
|
|
||||||
for page in pages:
|
for page in pages:
|
||||||
if 'Contents' in page:
|
if "Contents" in page:
|
||||||
for obj in page['Contents']:
|
for obj in page["Contents"]:
|
||||||
result.append(obj['Key'])
|
result.append(obj["Key"])
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def process_file(self, path: str, processor_func: Callable, **kwargs):
|
def process_file(self, path: str, processor_func: Callable, **kwargs):
|
||||||
@@ -98,22 +100,24 @@ class S3Storage(BaseStorage):
|
|||||||
path: Path to the file
|
path: Path to the file
|
||||||
processor_func: Function that processes the file
|
processor_func: Function that processes the file
|
||||||
**kwargs: Additional arguments to pass to the processor function
|
**kwargs: Additional arguments to pass to the processor function
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The result of the processor function
|
The result of the processor function
|
||||||
"""
|
"""
|
||||||
import tempfile
|
|
||||||
import logging
|
import logging
|
||||||
|
import tempfile
|
||||||
|
|
||||||
if not self.file_exists(path):
|
if not self.file_exists(path):
|
||||||
raise FileNotFoundError(f"File not found in S3: {path}")
|
raise FileNotFoundError(f"File not found in S3: {path}")
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
with tempfile.NamedTemporaryFile(suffix=os.path.splitext(path)[1], delete=True) as temp_file:
|
suffix=os.path.splitext(path)[1], delete=True
|
||||||
|
) as temp_file:
|
||||||
try:
|
try:
|
||||||
# Download the file from S3 to the temporary file
|
# Download the file from S3 to the temporary file
|
||||||
|
|
||||||
self.s3.download_fileobj(self.bucket_name, path, temp_file)
|
self.s3.download_fileobj(self.bucket_name, path, temp_file)
|
||||||
temp_file.flush()
|
temp_file.flush()
|
||||||
|
|
||||||
return processor_func(local_path=temp_file.name, **kwargs)
|
return processor_func(local_path=temp_file.name, **kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error processing S3 file {path}: {e}", exc_info=True)
|
logging.error(f"Error processing S3 file {path}: {e}", exc_info=True)
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import uuid
|
||||||
|
|
||||||
import tiktoken
|
import tiktoken
|
||||||
from flask import jsonify, make_response
|
from flask import jsonify, make_response
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
from application.core.settings import settings
|
||||||
|
|
||||||
|
|
||||||
_encoding = None
|
_encoding = None
|
||||||
@@ -15,6 +19,31 @@ def get_encoding():
|
|||||||
return _encoding
|
return _encoding
|
||||||
|
|
||||||
|
|
||||||
|
def safe_filename(filename):
|
||||||
|
"""
|
||||||
|
Creates a safe filename that preserves the original extension.
|
||||||
|
Uses secure_filename, but ensures a proper filename is returned even with non-Latin characters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename (str): The original filename
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A safe filename that can be used for storage
|
||||||
|
"""
|
||||||
|
if not filename:
|
||||||
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
_, extension = os.path.splitext(filename)
|
||||||
|
|
||||||
|
safe_name = secure_filename(filename)
|
||||||
|
|
||||||
|
# If secure_filename returns just the extension or an empty string
|
||||||
|
if not safe_name or safe_name == extension.lstrip("."):
|
||||||
|
return f"{str(uuid.uuid4())}{extension}"
|
||||||
|
|
||||||
|
return safe_name
|
||||||
|
|
||||||
|
|
||||||
def num_tokens_from_string(string: str) -> int:
|
def num_tokens_from_string(string: str) -> int:
|
||||||
encoding = get_encoding()
|
encoding = get_encoding()
|
||||||
if isinstance(string, str):
|
if isinstance(string, str):
|
||||||
@@ -74,8 +103,8 @@ def limit_chat_history(history, max_token_limit=None, gpt_model="docsgpt"):
|
|||||||
max_token_limit
|
max_token_limit
|
||||||
if max_token_limit
|
if max_token_limit
|
||||||
and max_token_limit
|
and max_token_limit
|
||||||
< settings.MODEL_TOKEN_LIMITS.get(gpt_model, settings.DEFAULT_MAX_HISTORY)
|
< settings.LLM_TOKEN_LIMITS.get(gpt_model, settings.DEFAULT_MAX_HISTORY)
|
||||||
else settings.MODEL_TOKEN_LIMITS.get(gpt_model, settings.DEFAULT_MAX_HISTORY)
|
else settings.LLM_TOKEN_LIMITS.get(gpt_model, settings.DEFAULT_MAX_HISTORY)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not history:
|
if not history:
|
||||||
@@ -109,3 +138,14 @@ def validate_function_name(function_name):
|
|||||||
if not re.match(r"^[a-zA-Z0-9_-]+$", function_name):
|
if not re.match(r"^[a-zA-Z0-9_-]+$", function_name):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def generate_image_url(image_path):
|
||||||
|
strategy = getattr(settings, "URL_STRATEGY", "backend")
|
||||||
|
if strategy == "s3":
|
||||||
|
bucket_name = getattr(settings, "S3_BUCKET_NAME", "docsgpt-test-bucket")
|
||||||
|
region_name = getattr(settings, "SAGEMAKER_REGION", "eu-central-1")
|
||||||
|
return f"https://{bucket_name}.s3.{region_name}.amazonaws.com/{image_path}"
|
||||||
|
else:
|
||||||
|
base_url = getattr(settings, "API_URL", "http://localhost:7091")
|
||||||
|
return f"{base_url}/api/images/{image_path}"
|
||||||
|
|||||||
@@ -143,8 +143,8 @@ def run_agent_logic(agent_config, input_data):
|
|||||||
agent = AgentCreator.create_agent(
|
agent = AgentCreator.create_agent(
|
||||||
agent_type,
|
agent_type,
|
||||||
endpoint="webhook",
|
endpoint="webhook",
|
||||||
llm_name=settings.LLM_NAME,
|
llm_name=settings.LLM_PROVIDER,
|
||||||
gpt_model=settings.MODEL_NAME,
|
gpt_model=settings.LLM_NAME,
|
||||||
api_key=settings.API_KEY,
|
api_key=settings.API_KEY,
|
||||||
user_api_key=user_api_key,
|
user_api_key=user_api_key,
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
@@ -159,7 +159,7 @@ def run_agent_logic(agent_config, input_data):
|
|||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
chunks=chunks,
|
chunks=chunks,
|
||||||
token_limit=settings.DEFAULT_MAX_HISTORY,
|
token_limit=settings.DEFAULT_MAX_HISTORY,
|
||||||
gpt_model=settings.MODEL_NAME,
|
gpt_model=settings.LLM_NAME,
|
||||||
user_api_key=user_api_key,
|
user_api_key=user_api_key,
|
||||||
decoded_token=decoded_token,
|
decoded_token=decoded_token,
|
||||||
)
|
)
|
||||||
@@ -194,7 +194,7 @@ def run_agent_logic(agent_config, input_data):
|
|||||||
|
|
||||||
# Define the main function for ingesting and processing documents.
|
# Define the main function for ingesting and processing documents.
|
||||||
def ingest_worker(
|
def ingest_worker(
|
||||||
self, directory, formats, name_job, filename, user, retriever="classic"
|
self, directory, formats, job_name, filename, user, dir_name=None, user_dir=None, retriever="classic"
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Ingest and process documents.
|
Ingest and process documents.
|
||||||
@@ -203,9 +203,11 @@ def ingest_worker(
|
|||||||
self: Reference to the instance of the task.
|
self: Reference to the instance of the task.
|
||||||
directory (str): Specifies the directory for ingesting ('inputs' or 'temp').
|
directory (str): Specifies the directory for ingesting ('inputs' or 'temp').
|
||||||
formats (list of str): List of file extensions to consider for ingestion (e.g., [".rst", ".md"]).
|
formats (list of str): List of file extensions to consider for ingestion (e.g., [".rst", ".md"]).
|
||||||
name_job (str): Name of the job for this ingestion task.
|
job_name (str): Name of the job for this ingestion task (original, unsanitized).
|
||||||
filename (str): Name of the file to be ingested.
|
filename (str): Name of the file to be ingested.
|
||||||
user (str): Identifier for the user initiating the ingestion.
|
user (str): Identifier for the user initiating the ingestion (original, unsanitized).
|
||||||
|
dir_name (str, optional): Sanitized directory name for filesystem operations.
|
||||||
|
user_dir (str, optional): Sanitized user ID for filesystem operations.
|
||||||
retriever (str): Type of retriever to use for processing the documents.
|
retriever (str): Type of retriever to use for processing the documents.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -216,13 +218,13 @@ def ingest_worker(
|
|||||||
limit = None
|
limit = None
|
||||||
exclude = True
|
exclude = True
|
||||||
sample = False
|
sample = False
|
||||||
|
|
||||||
storage = StorageCreator.get_storage()
|
storage = StorageCreator.get_storage()
|
||||||
|
|
||||||
full_path = os.path.join(directory, user, name_job)
|
full_path = os.path.join(directory, user_dir, dir_name)
|
||||||
source_file_path = os.path.join(full_path, filename)
|
source_file_path = os.path.join(full_path, filename)
|
||||||
|
|
||||||
logging.info(f"Ingest file: {full_path}", extra={"user": user, "job": name_job})
|
logging.info(f"Ingest file: {full_path}", extra={"user": user, "job": job_name})
|
||||||
|
|
||||||
# Create temporary working directory
|
# Create temporary working directory
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
@@ -283,13 +285,14 @@ def ingest_worker(
|
|||||||
for i in range(min(5, len(raw_docs))):
|
for i in range(min(5, len(raw_docs))):
|
||||||
logging.info(f"Sample document {i}: {raw_docs[i]}")
|
logging.info(f"Sample document {i}: {raw_docs[i]}")
|
||||||
file_data = {
|
file_data = {
|
||||||
"name": name_job,
|
"name": job_name, # Use original job_name
|
||||||
"file": filename,
|
"file": filename,
|
||||||
"user": user,
|
"user": user, # Use original user
|
||||||
"tokens": tokens,
|
"tokens": tokens,
|
||||||
"retriever": retriever,
|
"retriever": retriever,
|
||||||
"id": str(id),
|
"id": str(id),
|
||||||
"type": "local",
|
"type": "local",
|
||||||
|
"original_file_path": source_file_path,
|
||||||
}
|
}
|
||||||
|
|
||||||
upload_index(vector_store_path, file_data)
|
upload_index(vector_store_path, file_data)
|
||||||
@@ -301,9 +304,9 @@ def ingest_worker(
|
|||||||
return {
|
return {
|
||||||
"directory": directory,
|
"directory": directory,
|
||||||
"formats": formats,
|
"formats": formats,
|
||||||
"name_job": name_job,
|
"name_job": job_name, # Use original job_name
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"user": user,
|
"user": user, # Use original user
|
||||||
"limited": False,
|
"limited": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,7 +452,7 @@ def attachment_worker(self, file_info, user):
|
|||||||
try:
|
try:
|
||||||
self.update_state(state="PROGRESS", meta={"current": 10})
|
self.update_state(state="PROGRESS", meta={"current": 10})
|
||||||
storage = StorageCreator.get_storage()
|
storage = StorageCreator.get_storage()
|
||||||
|
|
||||||
self.update_state(
|
self.update_state(
|
||||||
state="PROGRESS", meta={"current": 30, "status": "Processing content"}
|
state="PROGRESS", meta={"current": 30, "status": "Processing content"}
|
||||||
)
|
)
|
||||||
@@ -458,9 +461,11 @@ def attachment_worker(self, file_info, user):
|
|||||||
relative_path,
|
relative_path,
|
||||||
lambda local_path, **kwargs: SimpleDirectoryReader(
|
lambda local_path, **kwargs: SimpleDirectoryReader(
|
||||||
input_files=[local_path], exclude_hidden=True, errors="ignore"
|
input_files=[local_path], exclude_hidden=True, errors="ignore"
|
||||||
).load_data()[0].text
|
)
|
||||||
|
.load_data()[0]
|
||||||
|
.text,
|
||||||
)
|
)
|
||||||
|
|
||||||
token_count = num_tokens_from_string(content)
|
token_count = num_tokens_from_string(content)
|
||||||
|
|
||||||
self.update_state(
|
self.update_state(
|
||||||
@@ -475,6 +480,7 @@ def attachment_worker(self, file_info, user):
|
|||||||
"_id": doc_id,
|
"_id": doc_id,
|
||||||
"user": user,
|
"user": user,
|
||||||
"path": relative_path,
|
"path": relative_path,
|
||||||
|
"filename": filename,
|
||||||
"content": content,
|
"content": content,
|
||||||
"token_count": token_count,
|
"token_count": token_count,
|
||||||
"mime_type": mime_type,
|
"mime_type": mime_type,
|
||||||
@@ -487,9 +493,7 @@ def attachment_worker(self, file_info, user):
|
|||||||
f"Stored attachment with ID: {attachment_id}", extra={"user": user}
|
f"Stored attachment with ID: {attachment_id}", extra={"user": user}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.update_state(
|
self.update_state(state="PROGRESS", meta={"current": 100, "status": "Complete"})
|
||||||
state="PROGRESS", meta={"current": 100, "status": "Complete"}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
name: docsgpt-oss
|
||||||
services:
|
services:
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
name: docsgpt-oss
|
||||||
services:
|
services:
|
||||||
frontend:
|
frontend:
|
||||||
build: ../frontend
|
build: ../frontend
|
||||||
@@ -17,19 +18,19 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- API_KEY=$API_KEY
|
- API_KEY=$API_KEY
|
||||||
- EMBEDDINGS_KEY=$API_KEY
|
- EMBEDDINGS_KEY=$API_KEY
|
||||||
|
- LLM_PROVIDER=$LLM_PROVIDER
|
||||||
- LLM_NAME=$LLM_NAME
|
- LLM_NAME=$LLM_NAME
|
||||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||||
- CELERY_RESULT_BACKEND=redis://redis:6379/1
|
- CELERY_RESULT_BACKEND=redis://redis:6379/1
|
||||||
- MONGO_URI=mongodb://mongo:27017/docsgpt
|
- MONGO_URI=mongodb://mongo:27017/docsgpt
|
||||||
- CACHE_REDIS_URL=redis://redis:6379/2
|
- CACHE_REDIS_URL=redis://redis:6379/2
|
||||||
- OPENAI_BASE_URL=$OPENAI_BASE_URL
|
- OPENAI_BASE_URL=$OPENAI_BASE_URL
|
||||||
- MODEL_NAME=$MODEL_NAME
|
|
||||||
ports:
|
ports:
|
||||||
- "7091:7091"
|
- "7091:7091"
|
||||||
volumes:
|
volumes:
|
||||||
- ../application/indexes:/app/application/indexes
|
- ../application/indexes:/app/indexes
|
||||||
- ../application/inputs:/app/inputs
|
- ../application/inputs:/app/inputs
|
||||||
- ../application/vectors:/app/application/vectors
|
- ../application/vectors:/app/vectors
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- mongo
|
- mongo
|
||||||
@@ -41,6 +42,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- API_KEY=$API_KEY
|
- API_KEY=$API_KEY
|
||||||
- EMBEDDINGS_KEY=$API_KEY
|
- EMBEDDINGS_KEY=$API_KEY
|
||||||
|
- LLM_PROVIDER=$LLM_PROVIDER
|
||||||
- LLM_NAME=$LLM_NAME
|
- LLM_NAME=$LLM_NAME
|
||||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||||
- CELERY_RESULT_BACKEND=redis://redis:6379/1
|
- CELERY_RESULT_BACKEND=redis://redis:6379/1
|
||||||
@@ -48,9 +50,9 @@ services:
|
|||||||
- API_URL=http://backend:7091
|
- API_URL=http://backend:7091
|
||||||
- CACHE_REDIS_URL=redis://redis:6379/2
|
- CACHE_REDIS_URL=redis://redis:6379/2
|
||||||
volumes:
|
volumes:
|
||||||
- ../application/indexes:/app/application/indexes
|
- ../application/indexes:/app/indexes
|
||||||
- ../application/inputs:/app/inputs
|
- ../application/inputs:/app/inputs
|
||||||
- ../application/vectors:/app/application/vectors
|
- ../application/vectors:/app/vectors
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- mongo
|
- mongo
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ metadata:
|
|||||||
name: docsgpt-secrets
|
name: docsgpt-secrets
|
||||||
type: Opaque
|
type: Opaque
|
||||||
data:
|
data:
|
||||||
LLM_NAME: ZG9jc2dwdA==
|
LLM_PROVIDER: ZG9jc2dwdA==
|
||||||
INTERNAL_KEY: aW50ZXJuYWw=
|
INTERNAL_KEY: aW50ZXJuYWw=
|
||||||
CELERY_BROKER_URL: cmVkaXM6Ly9yZWRpcy1zZXJ2aWNlOjYzNzkvMA==
|
CELERY_BROKER_URL: cmVkaXM6Ly9yZWRpcy1zZXJ2aWNlOjYzNzkvMA==
|
||||||
CELERY_RESULT_BACKEND: cmVkaXM6Ly9yZWRpcy1zZXJ2aWNlOjYzNzkvMA==
|
CELERY_RESULT_BACKEND: cmVkaXM6Ly9yZWRpcy1zZXJ2aWNlOjYzNzkvMA==
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ The fastest way to try out DocsGPT is by using the public API endpoint. This req
|
|||||||
Open the `.env` file and add the following lines:
|
Open the `.env` file and add the following lines:
|
||||||
|
|
||||||
```
|
```
|
||||||
LLM_NAME=docsgpt
|
LLM_PROVIDER=docsgpt
|
||||||
VITE_API_STREAMING=true
|
VITE_API_STREAMING=true
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -93,16 +93,16 @@ There are two Ollama optional files:
|
|||||||
|
|
||||||
3. **Pull the Ollama Model:**
|
3. **Pull the Ollama Model:**
|
||||||
|
|
||||||
**Crucially, after launching with Ollama, you need to pull the desired model into the Ollama container.** Find the `MODEL_NAME` you configured in your `.env` file (e.g., `llama3.2:1b`). Then execute the following command to pull the model *inside* the running Ollama container:
|
**Crucially, after launching with Ollama, you need to pull the desired model into the Ollama container.** Find the `LLM_NAME` you configured in your `.env` file (e.g., `llama3.2:1b`). Then execute the following command to pull the model *inside* the running Ollama container:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-cpu.yaml exec -it ollama ollama pull <MODEL_NAME>
|
docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-cpu.yaml exec -it ollama ollama pull <LLM_NAME>
|
||||||
```
|
```
|
||||||
or (for GPU):
|
or (for GPU):
|
||||||
```bash
|
```bash
|
||||||
docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-gpu.yaml exec -it ollama ollama pull <MODEL_NAME>
|
docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-gpu.yaml exec -it ollama ollama pull <LLM_NAME>
|
||||||
```
|
```
|
||||||
Replace `<MODEL_NAME>` with the actual model name from your `.env` file.
|
Replace `<LLM_NAME>` with the actual model name from your `.env` file.
|
||||||
|
|
||||||
4. **Access DocsGPT in your browser:**
|
4. **Access DocsGPT in your browser:**
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ The easiest and recommended way to configure basic settings is by using a `.env`
|
|||||||
**Example `.env` file structure:**
|
**Example `.env` file structure:**
|
||||||
|
|
||||||
```
|
```
|
||||||
LLM_NAME=openai
|
LLM_PROVIDER=openai
|
||||||
API_KEY=YOUR_OPENAI_API_KEY
|
API_KEY=YOUR_OPENAI_API_KEY
|
||||||
MODEL_NAME=gpt-4o
|
LLM_NAME=gpt-4o
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Configuration via `settings.py` file (Advanced)
|
### 2. Configuration via `settings.py` file (Advanced)
|
||||||
@@ -37,7 +37,7 @@ While modifying `settings.py` offers more flexibility, it's generally recommende
|
|||||||
|
|
||||||
Here are some of the most fundamental settings you'll likely want to configure:
|
Here are some of the most fundamental settings you'll likely want to configure:
|
||||||
|
|
||||||
- **`LLM_NAME`**: This setting determines which Large Language Model (LLM) provider DocsGPT will use. It tells DocsGPT which API to interact with.
|
- **`LLM_PROVIDER`**: This setting determines which Large Language Model (LLM) provider DocsGPT will use. It tells DocsGPT which API to interact with.
|
||||||
|
|
||||||
- **Common values:**
|
- **Common values:**
|
||||||
- `docsgpt`: Use the DocsGPT Public API Endpoint (simple and free, as offered in `setup.sh` option 1).
|
- `docsgpt`: Use the DocsGPT Public API Endpoint (simple and free, as offered in `setup.sh` option 1).
|
||||||
@@ -49,11 +49,11 @@ Here are some of the most fundamental settings you'll likely want to configure:
|
|||||||
- `azure_openai`: Use Azure OpenAI Service.
|
- `azure_openai`: Use Azure OpenAI Service.
|
||||||
- `openai` (when using local inference engines like Ollama, Llama.cpp, TGI, etc.): This signals DocsGPT to use an OpenAI-compatible API format, even if the actual LLM is running locally.
|
- `openai` (when using local inference engines like Ollama, Llama.cpp, TGI, etc.): This signals DocsGPT to use an OpenAI-compatible API format, even if the actual LLM is running locally.
|
||||||
|
|
||||||
- **`MODEL_NAME`**: Specifies the specific model to use from the chosen LLM provider. The available models depend on the `LLM_NAME` you've selected.
|
- **`LLM_NAME`**: Specifies the specific model to use from the chosen LLM provider. The available models depend on the `LLM_PROVIDER` you've selected.
|
||||||
|
|
||||||
- **Examples:**
|
- **Examples:**
|
||||||
- For `LLM_NAME=openai`: `gpt-4o`
|
- For `LLM_PROVIDER=openai`: `gpt-4o`
|
||||||
- For `LLM_NAME=google`: `gemini-2.0-flash`
|
- For `LLM_PROVIDER=google`: `gemini-2.0-flash`
|
||||||
- For local models (e.g., Ollama): `llama3.2:1b` (or any model name available in your setup).
|
- For local models (e.g., Ollama): `llama3.2:1b` (or any model name available in your setup).
|
||||||
|
|
||||||
- **`EMBEDDINGS_NAME`**: This setting defines which embedding model DocsGPT will use to generate vector embeddings for your documents. Embeddings are numerical representations of text that allow DocsGPT to understand the semantic meaning of your documents for efficient search and retrieval.
|
- **`EMBEDDINGS_NAME`**: This setting defines which embedding model DocsGPT will use to generate vector embeddings for your documents. Embeddings are numerical representations of text that allow DocsGPT to understand the semantic meaning of your documents for efficient search and retrieval.
|
||||||
@@ -63,7 +63,7 @@ Here are some of the most fundamental settings you'll likely want to configure:
|
|||||||
|
|
||||||
- **`API_KEY`**: Required for most cloud-based LLM providers. This is your authentication key to access the LLM provider's API. You'll need to obtain this key from your chosen provider's platform.
|
- **`API_KEY`**: Required for most cloud-based LLM providers. This is your authentication key to access the LLM provider's API. You'll need to obtain this key from your chosen provider's platform.
|
||||||
|
|
||||||
- **`OPENAI_BASE_URL`**: Specifically used when `LLM_NAME` is set to `openai` but you are connecting to a local inference engine (like Ollama, Llama.cpp, etc.) that exposes an OpenAI-compatible API. This setting tells DocsGPT where to find your local LLM server.
|
- **`OPENAI_BASE_URL`**: Specifically used when `LLM_PROVIDER` is set to `openai` but you are connecting to a local inference engine (like Ollama, Llama.cpp, etc.) that exposes an OpenAI-compatible API. This setting tells DocsGPT where to find your local LLM server.
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
@@ -74,9 +74,9 @@ Let's look at some concrete examples of how to configure these settings in your
|
|||||||
To use OpenAI's `gpt-4o` model, you would configure your `.env` file like this:
|
To use OpenAI's `gpt-4o` model, you would configure your `.env` file like this:
|
||||||
|
|
||||||
```
|
```
|
||||||
LLM_NAME=openai
|
LLM_PROVIDER=openai
|
||||||
API_KEY=YOUR_OPENAI_API_KEY # Replace with your actual OpenAI API key
|
API_KEY=YOUR_OPENAI_API_KEY # Replace with your actual OpenAI API key
|
||||||
MODEL_NAME=gpt-4o
|
LLM_NAME=gpt-4o
|
||||||
```
|
```
|
||||||
|
|
||||||
Make sure to replace `YOUR_OPENAI_API_KEY` with your actual OpenAI API key.
|
Make sure to replace `YOUR_OPENAI_API_KEY` with your actual OpenAI API key.
|
||||||
@@ -86,14 +86,14 @@ Make sure to replace `YOUR_OPENAI_API_KEY` with your actual OpenAI API key.
|
|||||||
To use a local Ollama server with the `llama3.2:1b` model, you would configure your `.env` file like this:
|
To use a local Ollama server with the `llama3.2:1b` model, you would configure your `.env` file like this:
|
||||||
|
|
||||||
```
|
```
|
||||||
LLM_NAME=openai # Using OpenAI compatible API format for local models
|
LLM_PROVIDER=openai # Using OpenAI compatible API format for local models
|
||||||
API_KEY=None # API Key is not needed for local Ollama
|
API_KEY=None # API Key is not needed for local Ollama
|
||||||
MODEL_NAME=llama3.2:1b
|
LLM_NAME=llama3.2:1b
|
||||||
OPENAI_BASE_URL=http://host.docker.internal:11434/v1 # Default Ollama API URL within Docker
|
OPENAI_BASE_URL=http://host.docker.internal:11434/v1 # Default Ollama API URL within Docker
|
||||||
EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2 # You can also run embeddings locally if needed
|
EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2 # You can also run embeddings locally if needed
|
||||||
```
|
```
|
||||||
|
|
||||||
In this case, even though you are using Ollama locally, `LLM_NAME` is set to `openai` because Ollama (and many other local inference engines) are designed to be API-compatible with OpenAI. `OPENAI_BASE_URL` points DocsGPT to the local Ollama server.
|
In this case, even though you are using Ollama locally, `LLM_PROVIDER` is set to `openai` because Ollama (and many other local inference engines) are designed to be API-compatible with OpenAI. `OPENAI_BASE_URL` points DocsGPT to the local Ollama server.
|
||||||
|
|
||||||
## Authentication Settings
|
## Authentication Settings
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ Choose the LLM of your choice.
|
|||||||
### For Open source llm change:
|
### For Open source llm change:
|
||||||
<Steps>
|
<Steps>
|
||||||
### Step 1
|
### Step 1
|
||||||
For open source version please edit `LLM_NAME`, `MODEL_NAME` and others in the .env file. Refer to [⚙️ App Configuration](/Deploying/DocsGPT-Settings) for more information.
|
For open source version please edit `LLM_PROVIDER`, `LLM_NAME` and others in the .env file. Refer to [⚙️ App Configuration](/Deploying/DocsGPT-Settings) for more information.
|
||||||
### Step 2
|
### Step 2
|
||||||
Visit [☁️ Cloud Providers](/Models/cloud-providers) for the updated list of online models. Make sure you have the right API_KEY and correct LLM_NAME.
|
Visit [☁️ Cloud Providers](/Models/cloud-providers) for the updated list of online models. Make sure you have the right API_KEY and correct LLM_PROVIDER.
|
||||||
For self-hosted please visit [🖥️ Local Inference](/Models/local-inference).
|
For self-hosted please visit [🖥️ Local Inference](/Models/local-inference).
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ The primary method for configuring your LLM provider in DocsGPT is through the `
|
|||||||
|
|
||||||
To connect to a cloud LLM provider, you will typically need to configure the following basic settings in your `.env` file:
|
To connect to a cloud LLM provider, you will typically need to configure the following basic settings in your `.env` file:
|
||||||
|
|
||||||
* **`LLM_NAME`**: This setting is essential and identifies the specific cloud provider you wish to use (e.g., `openai`, `google`, `anthropic`).
|
* **`LLM_PROVIDER`**: This setting is essential and identifies the specific cloud provider you wish to use (e.g., `openai`, `google`, `anthropic`).
|
||||||
* **`MODEL_NAME`**: Specifies the exact model you want to utilize from your chosen provider (e.g., `gpt-4o`, `gemini-2.0-flash`, `claude-3-5-sonnet-latest`). Refer to your provider's documentation for a list of available models.
|
* **`LLM_NAME`**: Specifies the exact model you want to utilize from your chosen provider (e.g., `gpt-4o`, `gemini-2.0-flash`, `claude-3-5-sonnet-latest`). Refer to your provider's documentation for a list of available models.
|
||||||
* **`API_KEY`**: Almost all cloud LLM providers require an API key for authentication. Obtain your API key from your chosen provider's platform and securely store it in your `.env` file.
|
* **`API_KEY`**: Almost all cloud LLM providers require an API key for authentication. Obtain your API key from your chosen provider's platform and securely store it in your `.env` file.
|
||||||
|
|
||||||
## Explicitly Supported Cloud Providers
|
## Explicitly Supported Cloud Providers
|
||||||
|
|
||||||
DocsGPT offers direct, streamlined support for the following cloud LLM providers, making configuration straightforward. The table below outlines the `LLM_NAME` and example `MODEL_NAME` values to use for each provider in your `.env` file.
|
DocsGPT offers direct, streamlined support for the following cloud LLM providers, making configuration straightforward. The table below outlines the `LLM_PROVIDER` and example `LLM_NAME` values to use for each provider in your `.env` file.
|
||||||
|
|
||||||
| Provider | `LLM_NAME` | Example `MODEL_NAME` |
|
| Provider | `LLM_PROVIDER` | Example `LLM_NAME` |
|
||||||
| :--------------------------- | :------------- | :-------------------------- |
|
| :--------------------------- | :------------- | :-------------------------- |
|
||||||
| DocsGPT Public API | `docsgpt` | `None` |
|
| DocsGPT Public API | `docsgpt` | `None` |
|
||||||
| OpenAI | `openai` | `gpt-4o` |
|
| OpenAI | `openai` | `gpt-4o` |
|
||||||
@@ -35,16 +35,16 @@ DocsGPT offers direct, streamlined support for the following cloud LLM providers
|
|||||||
|
|
||||||
DocsGPT's flexible architecture allows you to connect to any cloud provider that offers an API compatible with the OpenAI API standard. This opens up a vast ecosystem of LLM services.
|
DocsGPT's flexible architecture allows you to connect to any cloud provider that offers an API compatible with the OpenAI API standard. This opens up a vast ecosystem of LLM services.
|
||||||
|
|
||||||
To connect to an OpenAI-compatible cloud provider, you will still use `LLM_NAME=openai` in your `.env` file. However, you will also need to specify the API endpoint of your chosen provider using the `OPENAI_BASE_URL` setting. You will also likely need to provide an `API_KEY` and `MODEL_NAME` as required by that provider.
|
To connect to an OpenAI-compatible cloud provider, you will still use `LLM_PROVIDER=openai` in your `.env` file. However, you will also need to specify the API endpoint of your chosen provider using the `OPENAI_BASE_URL` setting. You will also likely need to provide an `API_KEY` and `LLM_NAME` as required by that provider.
|
||||||
|
|
||||||
**Example for DeepSeek (OpenAI-Compatible API):**
|
**Example for DeepSeek (OpenAI-Compatible API):**
|
||||||
|
|
||||||
To connect to DeepSeek, which offers an OpenAI-compatible API, your `.env` file could be configured as follows:
|
To connect to DeepSeek, which offers an OpenAI-compatible API, your `.env` file could be configured as follows:
|
||||||
|
|
||||||
```
|
```
|
||||||
LLM_NAME=openai
|
LLM_PROVIDER=openai
|
||||||
API_KEY=YOUR_API_KEY # Your DeepSeek API key
|
API_KEY=YOUR_API_KEY # Your DeepSeek API key
|
||||||
MODEL_NAME=deepseek-chat # Or your desired DeepSeek model name
|
LLM_NAME=deepseek-chat # Or your desired DeepSeek model name
|
||||||
OPENAI_BASE_URL=https://api.deepseek.com/v1 # DeepSeek's OpenAI API URL
|
OPENAI_BASE_URL=https://api.deepseek.com/v1 # DeepSeek's OpenAI API URL
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ To use OpenAI's `text-embedding-ada-002` embedding model, you need to set `EMBED
|
|||||||
**Example `.env` configuration for OpenAI Embeddings:**
|
**Example `.env` configuration for OpenAI Embeddings:**
|
||||||
|
|
||||||
```
|
```
|
||||||
LLM_NAME=openai
|
LLM_PROVIDER=openai
|
||||||
API_KEY=YOUR_OPENAI_API_KEY # Your OpenAI API Key
|
API_KEY=YOUR_OPENAI_API_KEY # Your OpenAI API Key
|
||||||
EMBEDDINGS_NAME=openai_text-embedding-ada-002
|
EMBEDDINGS_NAME=openai_text-embedding-ada-002
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ Setting up a local inference engine with DocsGPT is configured through environme
|
|||||||
|
|
||||||
To connect to a local inference engine, you will generally need to configure these settings in your `.env` file:
|
To connect to a local inference engine, you will generally need to configure these settings in your `.env` file:
|
||||||
|
|
||||||
* **`LLM_NAME`**: Crucially set this to `openai`. This tells DocsGPT to use the OpenAI-compatible API format for communication, even though the LLM is local.
|
* **`LLM_PROVIDER`**: Crucially set this to `openai`. This tells DocsGPT to use the OpenAI-compatible API format for communication, even though the LLM is local.
|
||||||
* **`MODEL_NAME`**: Specify the model name as recognized by your local inference engine. This might be a model identifier or left as `None` if the engine doesn't require explicit model naming in the API request.
|
* **`LLM_NAME`**: Specify the model name as recognized by your local inference engine. This might be a model identifier or left as `None` if the engine doesn't require explicit model naming in the API request.
|
||||||
* **`OPENAI_BASE_URL`**: This is essential. Set this to the base URL of your local inference engine's API endpoint. This tells DocsGPT where to find your local LLM server.
|
* **`OPENAI_BASE_URL`**: This is essential. Set this to the base URL of your local inference engine's API endpoint. This tells DocsGPT where to find your local LLM server.
|
||||||
* **`API_KEY`**: Generally, for local inference engines, you can set `API_KEY=None` as authentication is usually not required in local setups.
|
* **`API_KEY`**: Generally, for local inference engines, you can set `API_KEY=None` as authentication is usually not required in local setups.
|
||||||
|
|
||||||
@@ -24,16 +24,16 @@ To connect to a local inference engine, you will generally need to configure the
|
|||||||
|
|
||||||
DocsGPT is readily configurable to work with the following local inference engines, all communicating via the OpenAI API format. Here are example `OPENAI_BASE_URL` values for each, based on default setups:
|
DocsGPT is readily configurable to work with the following local inference engines, all communicating via the OpenAI API format. Here are example `OPENAI_BASE_URL` values for each, based on default setups:
|
||||||
|
|
||||||
| Inference Engine | `LLM_NAME` | `OPENAI_BASE_URL` |
|
| Inference Engine | `LLM_PROVIDER` | `OPENAI_BASE_URL` |
|
||||||
| :---------------------------- | :--------- | :------------------------- |
|
| :---------------------------- | :------------- | :------------------------- |
|
||||||
| LLaMa.cpp | `openai` | `http://localhost:8000/v1` |
|
| LLaMa.cpp | `openai` | `http://localhost:8000/v1` |
|
||||||
| Ollama | `openai` | `http://localhost:11434/v1` |
|
| Ollama | `openai` | `http://localhost:11434/v1` |
|
||||||
| Text Generation Inference (TGI)| `openai` | `http://localhost:8080/v1` |
|
| Text Generation Inference (TGI)| `openai` | `http://localhost:8080/v1` |
|
||||||
| SGLang | `openai` | `http://localhost:30000/v1` |
|
| SGLang | `openai` | `http://localhost:30000/v1` |
|
||||||
| vLLM | `openai` | `http://localhost:8000/v1` |
|
| vLLM | `openai` | `http://localhost:8000/v1` |
|
||||||
| Aphrodite | `openai` | `http://localhost:2242/v1` |
|
| Aphrodite | `openai` | `http://localhost:2242/v1` |
|
||||||
| FriendliAI | `openai` | `http://localhost:8997/v1` |
|
| FriendliAI | `openai` | `http://localhost:8997/v1` |
|
||||||
| LMDeploy | `openai` | `http://localhost:23333/v1` |
|
| LMDeploy | `openai` | `http://localhost:23333/v1` |
|
||||||
|
|
||||||
**Important Note on `localhost` vs `host.docker.internal`:**
|
**Important Note on `localhost` vs `host.docker.internal`:**
|
||||||
|
|
||||||
|
|||||||
44
frontend/package-lock.json
generated
@@ -8,7 +8,7 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^2.5.1",
|
"@reduxjs/toolkit": "^2.8.2",
|
||||||
"chart.js": "^4.4.4",
|
"chart.js": "^4.4.4",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"i18next": "^24.2.0",
|
"i18next": "^24.2.0",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"react-chartjs-2": "^5.3.0",
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-dropzone": "^14.3.5",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
@@ -28,7 +28,8 @@
|
|||||||
"react-syntax-highlighter": "^15.6.1",
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-math": "^6.0.0"
|
"remark-math": "^6.0.0",
|
||||||
|
"tailwind-merge": "^3.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mermaid": "^9.1.0",
|
"@types/mermaid": "^9.1.0",
|
||||||
@@ -1197,11 +1198,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@reduxjs/toolkit": {
|
"node_modules/@reduxjs/toolkit": {
|
||||||
"version": "2.5.1",
|
"version": "2.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz",
|
||||||
"integrity": "sha512-UHhy3p0oUpdhnSxyDjaRDYaw8Xra75UiLbCiRozVPHjfDwNYkh0TsVm/1OmTW8Md+iDAJmYPWUKMvsMc2GtpNg==",
|
"integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@standard-schema/utils": "^0.3.0",
|
||||||
"immer": "^10.0.3",
|
"immer": "^10.0.3",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-thunk": "^3.1.0",
|
"redux-thunk": "^3.1.0",
|
||||||
@@ -1542,6 +1545,18 @@
|
|||||||
"integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
|
"integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
|
||||||
@@ -9186,9 +9201,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-dropzone": {
|
"node_modules/react-dropzone": {
|
||||||
"version": "14.3.5",
|
"version": "14.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz",
|
||||||
"integrity": "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==",
|
"integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"attr-accept": "^2.2.4",
|
"attr-accept": "^2.2.4",
|
||||||
"file-selector": "^2.1.0",
|
"file-selector": "^2.1.0",
|
||||||
@@ -10469,6 +10485,16 @@
|
|||||||
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwind-merge": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/dcastil"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.17",
|
"version": "3.4.17",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^2.5.1",
|
"@reduxjs/toolkit": "^2.8.2",
|
||||||
"chart.js": "^4.4.4",
|
"chart.js": "^4.4.4",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"i18next": "^24.2.0",
|
"i18next": "^24.2.0",
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"react-chartjs-2": "^5.3.0",
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-dropzone": "^14.3.5",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
@@ -39,7 +39,8 @@
|
|||||||
"react-syntax-highlighter": "^15.6.1",
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-math": "^6.0.0"
|
"remark-math": "^6.0.0",
|
||||||
|
"tailwind-merge": "^3.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mermaid": "^9.1.0",
|
"@types/mermaid": "^9.1.0",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
setConversations,
|
setConversations,
|
||||||
setModalStateDeleteConv,
|
setModalStateDeleteConv,
|
||||||
setSelectedAgent,
|
setSelectedAgent,
|
||||||
|
setSharedAgents,
|
||||||
} from './preferences/preferenceSlice';
|
} from './preferences/preferenceSlice';
|
||||||
import Upload from './upload/Upload';
|
import Upload from './upload/Upload';
|
||||||
|
|
||||||
@@ -169,73 +170,65 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
const handleTogglePin = (agent: Agent) => {
|
const handleTogglePin = (agent: Agent) => {
|
||||||
userService.togglePinAgent(agent.id ?? '', token).then((response) => {
|
userService.togglePinAgent(agent.id ?? '', token).then((response) => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const updatedAgents = agents?.map((a) =>
|
const updatePinnedStatus = (a: Agent) =>
|
||||||
a.id === agent.id ? { ...a, pinned: !a.pinned } : a,
|
a.id === agent.id ? { ...a, pinned: !a.pinned } : a;
|
||||||
);
|
dispatch(setAgents(agents?.map(updatePinnedStatus)));
|
||||||
dispatch(setAgents(updatedAgents));
|
dispatch(setSharedAgents(sharedAgents?.map(updatePinnedStatus)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConversationClick = (index: string) => {
|
const handleConversationClick = async (index: string) => {
|
||||||
dispatch(setSelectedAgent(null));
|
try {
|
||||||
conversationService
|
dispatch(setSelectedAgent(null));
|
||||||
.getConversation(index, token)
|
|
||||||
.then((response) => {
|
const response = await conversationService.getConversation(index, token);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
navigate('/');
|
navigate('/');
|
||||||
dispatch(setSelectedAgent(null));
|
return;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
return response.json();
|
const data = await response.json();
|
||||||
})
|
if (!data) return;
|
||||||
.then((data) => {
|
|
||||||
if (!data) return;
|
dispatch(setConversation(data.queries));
|
||||||
dispatch(setConversation(data.queries));
|
dispatch(updateConversationId({ query: { conversationId: index } }));
|
||||||
dispatch(
|
|
||||||
updateConversationId({
|
if (!data.agent_id) {
|
||||||
query: { conversationId: index },
|
navigate('/');
|
||||||
}),
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let agent: Agent;
|
||||||
|
if (data.is_shared_usage) {
|
||||||
|
const sharedResponse = await userService.getSharedAgent(
|
||||||
|
data.shared_token,
|
||||||
|
token,
|
||||||
);
|
);
|
||||||
if (isMobile || isTablet) {
|
if (!sharedResponse.ok) {
|
||||||
setNavOpen(false);
|
|
||||||
}
|
|
||||||
if (data.agent_id) {
|
|
||||||
if (data.is_shared_usage) {
|
|
||||||
userService
|
|
||||||
.getSharedAgent(data.shared_token, token)
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
navigate('/');
|
|
||||||
dispatch(setSelectedAgent(null));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
response.json().then((agent: Agent) => {
|
|
||||||
navigate(`/agents/shared/${agent.shared_token}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
userService.getAgent(data.agent_id, token).then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
navigate('/');
|
|
||||||
dispatch(setSelectedAgent(null));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
response.json().then((agent: Agent) => {
|
|
||||||
if (agent.shared_token)
|
|
||||||
navigate(`/agents/shared/${agent.shared_token}`);
|
|
||||||
else {
|
|
||||||
dispatch(setSelectedAgent(agent));
|
|
||||||
navigate('/');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
navigate('/');
|
navigate('/');
|
||||||
dispatch(setSelectedAgent(null));
|
return;
|
||||||
}
|
}
|
||||||
});
|
agent = await sharedResponse.json();
|
||||||
|
navigate(`/agents/shared/${agent.shared_token}`);
|
||||||
|
} else {
|
||||||
|
const agentResponse = await userService.getAgent(data.agent_id, token);
|
||||||
|
if (!agentResponse.ok) {
|
||||||
|
navigate('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
agent = await agentResponse.json();
|
||||||
|
if (agent.shared_token) {
|
||||||
|
navigate(`/agents/shared/${agent.shared_token}`);
|
||||||
|
} else {
|
||||||
|
await Promise.resolve(dispatch(setSelectedAgent(agent)));
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling conversation click:', error);
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetConversation = () => {
|
const resetConversation = () => {
|
||||||
@@ -408,9 +401,13 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex w-6 justify-center">
|
<div className="flex w-6 justify-center">
|
||||||
<img
|
<img
|
||||||
src={agent.image ?? Robot}
|
src={
|
||||||
|
agent.image && agent.image.trim() !== ''
|
||||||
|
? agent.image
|
||||||
|
: Robot
|
||||||
|
}
|
||||||
alt="agent-logo"
|
alt="agent-logo"
|
||||||
className="h-6 w-6"
|
className="h-6 w-6 rounded-full object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">
|
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">
|
||||||
|
|||||||
@@ -83,9 +83,9 @@ export default function AgentCard({
|
|||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex w-full items-center gap-1 px-1">
|
<div className="flex w-full items-center gap-1 px-1">
|
||||||
<img
|
<img
|
||||||
src={agent.image ?? Robot}
|
src={agent.image && agent.image.trim() !== '' ? agent.image : Robot}
|
||||||
alt={`${agent.name}`}
|
alt={`${agent.name}`}
|
||||||
className="h-7 w-7 rounded-full"
|
className="h-7 w-7 rounded-full object-contain"
|
||||||
/>
|
/>
|
||||||
{agent.status === 'draft' && (
|
{agent.status === 'draft' && (
|
||||||
<p className="text-xs text-black opacity-50 dark:text-[#E0E0E0]">
|
<p className="text-xs text-black opacity-50 dark:text-[#E0E0E0]">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
@@ -6,6 +6,7 @@ import userService from '../api/services/userService';
|
|||||||
import ArrowLeft from '../assets/arrow-left.svg';
|
import ArrowLeft from '../assets/arrow-left.svg';
|
||||||
import SourceIcon from '../assets/source.svg';
|
import SourceIcon from '../assets/source.svg';
|
||||||
import Dropdown from '../components/Dropdown';
|
import Dropdown from '../components/Dropdown';
|
||||||
|
import { FileUpload } from '../components/FileUpload';
|
||||||
import MultiSelectPopup, { OptionType } from '../components/MultiSelectPopup';
|
import MultiSelectPopup, { OptionType } from '../components/MultiSelectPopup';
|
||||||
import AgentDetailsModal from '../modals/AgentDetailsModal';
|
import AgentDetailsModal from '../modals/AgentDetailsModal';
|
||||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||||
@@ -48,6 +49,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
agent_type: '',
|
agent_type: '',
|
||||||
status: '',
|
status: '',
|
||||||
});
|
});
|
||||||
|
const [imageFile, setImageFile] = useState<File | null>(null);
|
||||||
const [prompts, setPrompts] = useState<
|
const [prompts, setPrompts] = useState<
|
||||||
{ name: string; id: string; type: string }[]
|
{ name: string; id: string; type: string }[]
|
||||||
>([]);
|
>([]);
|
||||||
@@ -106,6 +108,13 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpload = useCallback((files: File[]) => {
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const file = files[0];
|
||||||
|
setImageFile(file);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
if (selectedAgent) dispatch(setSelectedAgent(null));
|
if (selectedAgent) dispatch(setSelectedAgent(null));
|
||||||
navigate('/agents');
|
navigate('/agents');
|
||||||
@@ -118,42 +127,80 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveDraft = async () => {
|
const handleSaveDraft = async () => {
|
||||||
const response =
|
const formData = new FormData();
|
||||||
effectiveMode === 'new'
|
formData.append('name', agent.name);
|
||||||
? await userService.createAgent({ ...agent, status: 'draft' }, token)
|
formData.append('description', agent.description);
|
||||||
: await userService.updateAgent(
|
formData.append('source', agent.source);
|
||||||
agent.id || '',
|
formData.append('chunks', agent.chunks);
|
||||||
{ ...agent, status: 'draft' },
|
formData.append('retriever', agent.retriever);
|
||||||
token,
|
formData.append('prompt_id', agent.prompt_id);
|
||||||
);
|
formData.append('agent_type', agent.agent_type);
|
||||||
if (!response.ok) throw new Error('Failed to create agent draft');
|
formData.append('status', 'draft');
|
||||||
const data = await response.json();
|
|
||||||
if (effectiveMode === 'new') {
|
if (imageFile) formData.append('image', imageFile);
|
||||||
setEffectiveMode('draft');
|
|
||||||
setAgent((prev) => ({ ...prev, id: data.id }));
|
if (agent.tools && agent.tools.length > 0)
|
||||||
|
formData.append('tools', JSON.stringify(agent.tools));
|
||||||
|
else formData.append('tools', '[]');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response =
|
||||||
|
effectiveMode === 'new'
|
||||||
|
? await userService.createAgent(formData, token)
|
||||||
|
: await userService.updateAgent(agent.id || '', formData, token);
|
||||||
|
if (!response.ok) throw new Error('Failed to create agent draft');
|
||||||
|
const data = await response.json();
|
||||||
|
if (effectiveMode === 'new') {
|
||||||
|
setEffectiveMode('draft');
|
||||||
|
setAgent((prev) => ({
|
||||||
|
...prev,
|
||||||
|
id: data.id,
|
||||||
|
image: data.image || prev.image,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving draft:', error);
|
||||||
|
throw new Error('Failed to save draft');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePublish = async () => {
|
const handlePublish = async () => {
|
||||||
const response =
|
const formData = new FormData();
|
||||||
effectiveMode === 'new'
|
formData.append('name', agent.name);
|
||||||
? await userService.createAgent(
|
formData.append('description', agent.description);
|
||||||
{ ...agent, status: 'published' },
|
formData.append('source', agent.source);
|
||||||
token,
|
formData.append('chunks', agent.chunks);
|
||||||
)
|
formData.append('retriever', agent.retriever);
|
||||||
: await userService.updateAgent(
|
formData.append('prompt_id', agent.prompt_id);
|
||||||
agent.id || '',
|
formData.append('agent_type', agent.agent_type);
|
||||||
{ ...agent, status: 'published' },
|
formData.append('status', 'published');
|
||||||
token,
|
|
||||||
);
|
if (imageFile) formData.append('image', imageFile);
|
||||||
if (!response.ok) throw new Error('Failed to publish agent');
|
if (agent.tools && agent.tools.length > 0)
|
||||||
const data = await response.json();
|
formData.append('tools', JSON.stringify(agent.tools));
|
||||||
if (data.id) setAgent((prev) => ({ ...prev, id: data.id }));
|
else formData.append('tools', '[]');
|
||||||
if (data.key) setAgent((prev) => ({ ...prev, key: data.key }));
|
|
||||||
if (effectiveMode === 'new' || effectiveMode === 'draft') {
|
try {
|
||||||
setEffectiveMode('edit');
|
const response =
|
||||||
setAgent((prev) => ({ ...prev, status: 'published' }));
|
effectiveMode === 'new'
|
||||||
setAgentDetails('ACTIVE');
|
? await userService.createAgent(formData, token)
|
||||||
|
: await userService.updateAgent(agent.id || '', formData, token);
|
||||||
|
if (!response.ok) throw new Error('Failed to publish agent');
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.id) setAgent((prev) => ({ ...prev, id: data.id }));
|
||||||
|
if (data.key) setAgent((prev) => ({ ...prev, key: data.key }));
|
||||||
|
if (effectiveMode === 'new' || effectiveMode === 'draft') {
|
||||||
|
setEffectiveMode('edit');
|
||||||
|
setAgent((prev) => ({
|
||||||
|
...prev,
|
||||||
|
status: 'published',
|
||||||
|
image: data.image || prev.image,
|
||||||
|
}));
|
||||||
|
setAgentDetails('ACTIVE');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error publishing agent:', error);
|
||||||
|
throw new Error('Failed to publish agent');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -325,6 +372,21 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
setAgent({ ...agent, description: e.target.value })
|
setAgent({ ...agent, description: e.target.value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<div className="mt-3">
|
||||||
|
<FileUpload
|
||||||
|
showPreview
|
||||||
|
className="dark:bg-[#222327]"
|
||||||
|
onUpload={handleUpload}
|
||||||
|
onRemove={() => setImageFile(null)}
|
||||||
|
uploadText={[
|
||||||
|
{ text: 'Click to upload', colorClass: 'text-[#7D54D1]' },
|
||||||
|
{
|
||||||
|
text: ' or drag and drop',
|
||||||
|
colorClass: 'text-[#525252]',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
|
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
|
||||||
<h2 className="text-lg font-semibold">Source</h2>
|
<h2 className="text-lg font-semibold">Source</h2>
|
||||||
@@ -333,7 +395,11 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
<button
|
<button
|
||||||
ref={sourceAnchorButtonRef}
|
ref={sourceAnchorButtonRef}
|
||||||
onClick={() => setIsSourcePopupOpen(!isSourcePopupOpen)}
|
onClick={() => setIsSourcePopupOpen(!isSourcePopupOpen)}
|
||||||
className="w-full truncate rounded-3xl border border-silver bg-white px-5 py-3 text-left text-sm text-gray-400 dark:border-[#7E7E7E] dark:bg-[#222327] dark:text-silver"
|
className={`w-full truncate rounded-3xl border border-silver bg-white px-5 py-3 text-left text-sm dark:border-[#7E7E7E] dark:bg-[#222327] ${
|
||||||
|
selectedSourceIds.size > 0
|
||||||
|
? 'text-jet dark:text-bright-gray'
|
||||||
|
: 'text-gray-400 dark:text-silver'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{selectedSourceIds.size > 0
|
{selectedSourceIds.size > 0
|
||||||
? Array.from(selectedSourceIds)
|
? Array.from(selectedSourceIds)
|
||||||
@@ -436,7 +502,11 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
|||||||
<button
|
<button
|
||||||
ref={toolAnchorButtonRef}
|
ref={toolAnchorButtonRef}
|
||||||
onClick={() => setIsToolsPopupOpen(!isToolsPopupOpen)}
|
onClick={() => setIsToolsPopupOpen(!isToolsPopupOpen)}
|
||||||
className="w-full truncate rounded-3xl border border-silver bg-white px-5 py-3 text-left text-sm text-gray-400 dark:border-[#7E7E7E] dark:bg-[#222327] dark:text-silver"
|
className={`w-full truncate rounded-3xl border border-silver bg-white px-5 py-3 text-left text-sm dark:border-[#7E7E7E] dark:bg-[#222327] ${
|
||||||
|
selectedToolIds.size > 0
|
||||||
|
? 'text-jet dark:text-bright-gray'
|
||||||
|
: 'text-gray-400 dark:text-silver'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{selectedToolIds.size > 0
|
{selectedToolIds.size > 0
|
||||||
? Array.from(selectedToolIds)
|
? Array.from(selectedToolIds)
|
||||||
|
|||||||
@@ -155,9 +155,13 @@ export default function SharedAgent() {
|
|||||||
<div className="relative h-full w-full">
|
<div className="relative h-full w-full">
|
||||||
<div className="absolute left-4 top-5 hidden items-center gap-3 sm:flex">
|
<div className="absolute left-4 top-5 hidden items-center gap-3 sm:flex">
|
||||||
<img
|
<img
|
||||||
src={sharedAgent.image ?? Robot}
|
src={
|
||||||
|
sharedAgent.image && sharedAgent.image.trim() !== ''
|
||||||
|
? sharedAgent.image
|
||||||
|
: Robot
|
||||||
|
}
|
||||||
alt="agent-logo"
|
alt="agent-logo"
|
||||||
className="h-6 w-6"
|
className="h-6 w-6 rounded-full object-contain"
|
||||||
/>
|
/>
|
||||||
<h2 className="text-lg font-semibold text-[#212121] dark:text-[#E0E0E0]">
|
<h2 className="text-lg font-semibold text-[#212121] dark:text-[#E0E0E0]">
|
||||||
{sharedAgent.name}
|
{sharedAgent.name}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ export default function SharedAgentCard({ agent }: { agent: Agent }) {
|
|||||||
<div className="flex w-full max-w-[720px] flex-col rounded-3xl border border-dark-gray p-6 shadow-sm dark:border-grey sm:w-fit sm:min-w-[480px]">
|
<div className="flex w-full max-w-[720px] flex-col rounded-3xl border border-dark-gray p-6 shadow-sm dark:border-grey sm:w-fit sm:min-w-[480px]">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex h-12 w-12 items-center justify-center overflow-hidden rounded-full p-1">
|
<div className="flex h-12 w-12 items-center justify-center overflow-hidden rounded-full p-1">
|
||||||
<img src={Robot} className="h-full w-full object-contain" />
|
<img
|
||||||
|
src={agent.image && agent.image.trim() !== '' ? agent.image : Robot}
|
||||||
|
className="h-full w-full rounded-full object-contain"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex max-h-[92px] w-[80%] flex-col gap-px">
|
<div className="flex max-h-[92px] w-[80%] flex-col gap-px">
|
||||||
<h2 className="text-base font-semibold text-[#212121] dark:text-[#E0E0E0] sm:text-lg">
|
<h2 className="text-base font-semibold text-[#212121] dark:text-[#E0E0E0] sm:text-lg">
|
||||||
|
|||||||
@@ -324,17 +324,21 @@ function AgentCard({
|
|||||||
iconWidth: 14,
|
iconWidth: 14,
|
||||||
iconHeight: 14,
|
iconHeight: 14,
|
||||||
},
|
},
|
||||||
{
|
...(agent.status === 'published'
|
||||||
icon: agent.pinned ? UnPin : Pin,
|
? [
|
||||||
label: agent.pinned ? 'Unpin' : 'Pin agent',
|
{
|
||||||
onClick: (e: SyntheticEvent) => {
|
icon: agent.pinned ? UnPin : Pin,
|
||||||
e.stopPropagation();
|
label: agent.pinned ? 'Unpin' : 'Pin agent',
|
||||||
togglePin();
|
onClick: (e: SyntheticEvent) => {
|
||||||
},
|
e.stopPropagation();
|
||||||
variant: 'primary',
|
togglePin();
|
||||||
iconWidth: 18,
|
},
|
||||||
iconHeight: 18,
|
variant: 'primary' as const,
|
||||||
},
|
iconWidth: 18,
|
||||||
|
iconHeight: 18,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
icon: Trash,
|
icon: Trash,
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
@@ -426,16 +430,16 @@ function AgentCard({
|
|||||||
setIsOpen={setIsMenuOpen}
|
setIsOpen={setIsMenuOpen}
|
||||||
options={menuOptions}
|
options={menuOptions}
|
||||||
anchorRef={menuRef}
|
anchorRef={menuRef}
|
||||||
position="top-right"
|
position="bottom-right"
|
||||||
offset={{ x: 0, y: 0 }}
|
offset={{ x: 0, y: 0 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex w-full items-center gap-1 px-1">
|
<div className="flex w-full items-center gap-1 px-1">
|
||||||
<img
|
<img
|
||||||
src={agent.image ?? Robot}
|
src={agent.image && agent.image.trim() !== '' ? agent.image : Robot}
|
||||||
alt={`${agent.name}`}
|
alt={`${agent.name}`}
|
||||||
className="h-7 w-7 rounded-full"
|
className="h-7 w-7 rounded-full object-contain"
|
||||||
/>
|
/>
|
||||||
{agent.status === 'draft' && (
|
{agent.status === 'draft' && (
|
||||||
<p className="text-xs text-black opacity-50 dark:text-[#E0E0E0]">{`(Draft)`}</p>
|
<p className="text-xs text-black opacity-50 dark:text-[#E0E0E0]">{`(Draft)`}</p>
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
export const baseURL =
|
export const baseURL =
|
||||||
import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
|
import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
|
||||||
|
|
||||||
const defaultHeaders = {
|
const getHeaders = (
|
||||||
'Content-Type': 'application/json',
|
token: string | null,
|
||||||
};
|
customHeaders = {},
|
||||||
|
isFormData = false,
|
||||||
const getHeaders = (token: string | null, customHeaders = {}): HeadersInit => {
|
): HeadersInit => {
|
||||||
return {
|
const headers: HeadersInit = {
|
||||||
...defaultHeaders,
|
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
...customHeaders,
|
...customHeaders,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isFormData) {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiClient = {
|
const apiClient = {
|
||||||
@@ -44,6 +49,21 @@ const apiClient = {
|
|||||||
return response;
|
return response;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
postFormData: (
|
||||||
|
url: string,
|
||||||
|
formData: FormData,
|
||||||
|
token: string | null,
|
||||||
|
headers = {},
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<Response> => {
|
||||||
|
return fetch(`${baseURL}${url}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getHeaders(token, headers, true),
|
||||||
|
body: formData,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
put: (
|
put: (
|
||||||
url: string,
|
url: string,
|
||||||
data: any,
|
data: any,
|
||||||
@@ -60,6 +80,21 @@ const apiClient = {
|
|||||||
return response;
|
return response;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
putFormData: (
|
||||||
|
url: string,
|
||||||
|
formData: FormData,
|
||||||
|
token: string | null,
|
||||||
|
headers = {},
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<Response> => {
|
||||||
|
return fetch(`${baseURL}${url}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: getHeaders(token, headers, true),
|
||||||
|
body: formData,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
delete: (
|
delete: (
|
||||||
url: string,
|
url: string,
|
||||||
token: string | null,
|
token: string | null,
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ const userService = {
|
|||||||
getAgents: (token: string | null): Promise<any> =>
|
getAgents: (token: string | null): Promise<any> =>
|
||||||
apiClient.get(endpoints.USER.AGENTS, token),
|
apiClient.get(endpoints.USER.AGENTS, token),
|
||||||
createAgent: (data: any, token: string | null): Promise<any> =>
|
createAgent: (data: any, token: string | null): Promise<any> =>
|
||||||
apiClient.post(endpoints.USER.CREATE_AGENT, data, token),
|
apiClient.postFormData(endpoints.USER.CREATE_AGENT, data, token),
|
||||||
updateAgent: (
|
updateAgent: (
|
||||||
agent_id: string,
|
agent_id: string,
|
||||||
data: any,
|
data: any,
|
||||||
token: string | null,
|
token: string | null,
|
||||||
): Promise<any> =>
|
): Promise<any> =>
|
||||||
apiClient.put(endpoints.USER.UPDATE_AGENT(agent_id), data, token),
|
apiClient.putFormData(endpoints.USER.UPDATE_AGENT(agent_id), data, token),
|
||||||
deleteAgent: (id: string, token: string | null): Promise<any> =>
|
deleteAgent: (id: string, token: string | null): Promise<any> =>
|
||||||
apiClient.delete(endpoints.USER.DELETE_AGENT(id), token),
|
apiClient.delete(endpoints.USER.DELETE_AGENT(id), token),
|
||||||
getPinnedAgents: (token: string | null): Promise<any> =>
|
getPinnedAgents: (token: string | null): Promise<any> =>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M8.16669 11.5H9.83335V13.1666H8.16669V11.5ZM8.16669 4.83329H9.83335V9.83329H8.16669V4.83329ZM8.99169 0.666626C4.39169 0.666626 0.666687 4.39996 0.666687 8.99996C0.666687 13.6 4.39169 17.3333 8.99169 17.3333C13.6 17.3333 17.3334 13.6 17.3334 8.99996C17.3334 4.39996 13.6 0.666626 8.99169 0.666626ZM9.00002 15.6666C5.31669 15.6666 2.33335 12.6833 2.33335 8.99996C2.33335 5.31663 5.31669 2.33329 9.00002 2.33329C12.6834 2.33329 15.6667 5.31663 15.6667 8.99996C15.6667 12.6833 12.6834 15.6666 9.00002 15.6666Z" fill="#F44336"/>
|
<path d="M8.16669 11.5H9.83335V13.1666H8.16669V11.5ZM8.16669 4.83329H9.83335V9.83329H8.16669V4.83329ZM8.99169 0.666626C4.39169 0.666626 0.666687 4.39996 0.666687 8.99996C0.666687 13.6 4.39169 17.3333 8.99169 17.3333C13.6 17.3333 17.3334 13.6 17.3334 8.99996C17.3334 4.39996 13.6 0.666626 8.99169 0.666626ZM9.00002 15.6666C5.31669 15.6666 2.33335 12.6833 2.33335 8.99996C2.33335 5.31663 5.31669 2.33329 9.00002 2.33329C12.6834 2.33329 15.6667 5.31663 15.6667 8.99996C15.6667 12.6833 12.6834 15.6666 9.00002 15.6666Z" fill="#ECECF1"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 636 B After Width: | Height: | Size: 636 B |
1
frontend/src/assets/cross.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||||
|
After Width: | Height: | Size: 262 B |
3
frontend/src/assets/external-link.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12.4028 7.35671V12.6427C12.4043 12.844 12.3655 13.0436 12.2889 13.2298C12.2122 13.4159 12.0991 13.5849 11.9564 13.7269C11.8136 13.8688 11.6439 13.9808 11.4573 14.0563C11.2706 14.1318 11.0708 14.1694 10.8695 14.1667H3.36483C3.16278 14.1693 2.96226 14.1314 2.77509 14.0553C2.58792 13.9791 2.41789 13.8663 2.27504 13.7234C2.13219 13.5804 2.01941 13.4104 1.94335 13.2232C1.86728 13.036 1.82948 12.8354 1.83217 12.6334V5.12871C1.82975 4.92668 1.86776 4.7262 1.94396 4.53908C2.02017 4.35196 2.13302 4.18196 2.27589 4.0391C2.41875 3.89623 2.58875 3.78338 2.77587 3.70717C2.963 3.63097 3.16347 3.59296 3.3655 3.59537H8.65083M14.1648 1.83337L7.1175 8.88071M14.1648 1.83337H10.6408M14.1648 1.83337V5.35737" stroke="#7D54D1" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 895 B |
3
frontend/src/assets/images.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="40" height="39" viewBox="0 0 40 39" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9477 3.02295H33.8827C35.898 3.02295 37.5388 4.6819 37.5388 6.71923V22.9819C37.5388 25.0193 35.898 26.6782 33.8827 26.6782H11.9477C9.9328 26.6782 8.29192 25.0193 8.29192 22.9819V6.71923C8.29192 4.6819 9.9328 3.02295 11.9477 3.02295ZM33.8827 5.97992H11.9477C11.5442 5.97992 11.2167 6.31098 11.2167 6.71916V20.6741L15.2527 16.595C16.2515 15.5839 17.8791 15.5839 18.8792 16.595L20.6486 18.3795L26.0799 11.7888C26.5653 11.2003 27.276 10.8603 28.0335 10.856C28.7953 10.8735 29.5046 11.1841 29.9946 11.765L34.614 17.2147V6.71923C34.614 6.31104 34.2866 5.97992 33.8827 5.97992ZM6.40446 25.1242C7.16068 27.3803 9.243 28.8957 11.584 28.8957H32.8128L31.4954 33.1312C31.1223 34.5715 29.7916 35.5487 28.3352 35.5487C28.051 35.5485 27.768 35.5117 27.4929 35.4393L4.88059 29.3169C3.13614 28.8305 2.09642 27.0048 2.55267 25.2438L6.10025 13.2714V23.3516C6.10025 23.8543 6.17497 24.3567 6.35333 24.9542L6.40446 25.1242ZM18.53 10.4151C18.53 12.0459 17.2186 13.3721 15.6055 13.3721C13.9926 13.3721 12.6808 12.0458 12.6808 10.4151C12.6808 8.78445 13.9925 7.45815 15.6055 7.45815C17.2186 7.45815 18.53 8.78438 18.53 10.4151Z" fill="#A3A3A3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,19 +1,19 @@
|
|||||||
<svg width="22" height="18" viewBox="0 0 22 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M13.394 1.001H7.982C7.32776 1.00074 6.67989 1.12939 6.07539 1.3796C5.47089 1.62982 4.92162 1.99669 4.45896 2.45926C3.9963 2.92182 3.62932 3.47102 3.37898 4.07547C3.12865 4.67992 2.99987 5.32776 3 5.982V11.394C2.99974 12.0483 3.12842 12.6963 3.3787 13.3008C3.62897 13.9054 3.99593 14.4547 4.45861 14.9174C4.92128 15.3801 5.4706 15.747 6.07516 15.9973C6.67972 16.2476 7.32768 16.3763 7.982 16.376H13.394C14.0483 16.3763 14.6963 16.2476 15.3008 15.9973C15.9054 15.747 16.4547 15.3801 16.9174 14.9174C17.3801 14.4547 17.747 13.9054 17.9973 13.3008C18.2476 12.6963 18.3763 12.0483 18.376 11.394V5.982C18.3763 5.32768 18.2476 4.67972 17.9973 4.07516C17.747 3.4706 17.3801 2.92128 16.9174 2.45861C16.4547 1.99593 15.9054 1.62897 15.3008 1.3787C14.6963 1.12842 14.0483 0.999738 13.394 1V1.001Z" stroke="url(#paint0_linear_8958_15228)" stroke-width="1.5"/>
|
<path d="M14.394 4.001H8.982C8.32776 4.00074 7.67989 4.12939 7.07539 4.3796C6.47089 4.62982 5.92162 4.99669 5.45896 5.45926C4.9963 5.92182 4.62932 6.47102 4.37898 7.07547C4.12865 7.67992 3.99987 8.32776 4 8.982V14.394C3.99974 15.0483 4.12842 15.6963 4.3787 16.3008C4.62897 16.9054 4.99593 17.4547 5.45861 17.9174C5.92128 18.3801 6.4706 18.747 7.07516 18.9973C7.67972 19.2476 8.32768 19.3763 8.982 19.376H14.394C15.0483 19.3763 15.6963 19.2476 16.3008 18.9973C16.9054 18.747 17.4547 18.3801 17.9174 17.9174C18.3801 17.4547 18.747 16.9054 18.9973 16.3008C19.2476 15.6963 19.3763 15.0483 19.376 14.394V8.982C19.3763 8.32768 19.2476 7.67972 18.9973 7.07516C18.747 6.4706 18.3801 5.92128 17.9174 5.45861C17.4547 4.99593 16.9054 4.62897 16.3008 4.3787C15.6963 4.12842 15.0483 3.99974 14.394 4V4.001Z" stroke="url(#paint0_linear_9044_3689)" stroke-width="1.5"/>
|
||||||
<path d="M18.606 12.5881H20.225C20.4968 12.5881 20.7576 12.4801 20.9498 12.2879C21.142 12.0956 21.25 11.8349 21.25 11.5631V6.43809C21.25 6.16624 21.142 5.90553 20.9498 5.7133C20.7576 5.52108 20.4968 5.41309 20.225 5.41309H18.605M3.395 12.5881H1.775C1.6404 12.5881 1.50711 12.5616 1.38275 12.5101C1.25839 12.4586 1.1454 12.3831 1.05022 12.2879C0.955035 12.1927 0.879535 12.0797 0.828023 11.9553C0.776512 11.831 0.75 11.6977 0.75 11.5631V6.43809C0.75 6.16624 0.857991 5.90553 1.05022 5.7133C1.24244 5.52108 1.50315 5.41309 1.775 5.41309H3.395" stroke="url(#paint1_linear_8958_15228)" stroke-width="1.5"/>
|
<path d="M19.606 15.5881H21.225C21.4968 15.5881 21.7576 15.4801 21.9498 15.2879C22.142 15.0956 22.25 14.8349 22.25 14.5631V9.43809C22.25 9.16624 22.142 8.90553 21.9498 8.7133C21.7576 8.52108 21.4968 8.41309 21.225 8.41309H19.605M4.395 15.5881H2.775C2.6404 15.5881 2.50711 15.5616 2.38275 15.5101C2.25839 15.4586 2.1454 15.3831 2.05022 15.2879C1.95504 15.1927 1.87953 15.0797 1.82802 14.9553C1.77651 14.831 1.75 14.6977 1.75 14.5631V9.43809C1.75 9.16624 1.85799 8.90553 2.05022 8.7133C2.24244 8.52108 2.50315 8.41309 2.775 8.41309H4.395" stroke="url(#paint1_linear_9044_3689)" stroke-width="1.5"/>
|
||||||
<path d="M1.76562 5.41323V1.31323M20.2256 5.41323L20.2156 1.31323M7.91562 5.76323V8.46123M14.0656 5.76323V8.46123M8.94062 12.5882H13.0406" stroke="url(#paint2_linear_8958_15228)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M2.76562 8.41323V4.31323M21.2256 8.41323L21.2156 4.31323M8.91562 8.76323V11.4612M15.0656 8.76323V11.4612M9.94062 15.5882H14.0406" stroke="url(#paint2_linear_9044_3689)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="paint0_linear_8958_15228" x1="10.688" y1="1" x2="10.688" y2="16.376" gradientUnits="userSpaceOnUse">
|
<linearGradient id="paint0_linear_9044_3689" x1="11.688" y1="4" x2="11.688" y2="19.376" gradientUnits="userSpaceOnUse">
|
||||||
<stop stop-color="#58E2E1"/>
|
<stop stop-color="#58E2E1"/>
|
||||||
<stop offset="0.524038" stop-color="#657797"/>
|
<stop offset="0.524038" stop-color="#657797"/>
|
||||||
<stop offset="1" stop-color="#CC7871"/>
|
<stop offset="1" stop-color="#CC7871"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient id="paint1_linear_8958_15228" x1="11" y1="5.41309" x2="11" y2="12.5881" gradientUnits="userSpaceOnUse">
|
<linearGradient id="paint1_linear_9044_3689" x1="12" y1="8.41309" x2="12" y2="15.5881" gradientUnits="userSpaceOnUse">
|
||||||
<stop stop-color="#58E2E1"/>
|
<stop stop-color="#58E2E1"/>
|
||||||
<stop offset="0.524038" stop-color="#657797"/>
|
<stop offset="0.524038" stop-color="#657797"/>
|
||||||
<stop offset="1" stop-color="#CC7871"/>
|
<stop offset="1" stop-color="#CC7871"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient id="paint2_linear_8958_15228" x1="10.9956" y1="1.31323" x2="10.9956" y2="12.5882" gradientUnits="userSpaceOnUse">
|
<linearGradient id="paint2_linear_9044_3689" x1="11.9956" y1="4.31323" x2="11.9956" y2="15.5882" gradientUnits="userSpaceOnUse">
|
||||||
<stop stop-color="#58E2E1"/>
|
<stop stop-color="#58E2E1"/>
|
||||||
<stop offset="0.524038" stop-color="#657797"/>
|
<stop offset="0.524038" stop-color="#657797"/>
|
||||||
<stop offset="1" stop-color="#CC7871"/>
|
<stop offset="1" stop-color="#CC7871"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
229
frontend/src/components/FileUpload.tsx
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDropzone } from 'react-dropzone';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
import Cross from '../assets/cross.svg';
|
||||||
|
import ImagesIcon from '../assets/images.svg';
|
||||||
|
|
||||||
|
interface FileUploadProps {
|
||||||
|
onUpload: (files: File[]) => void;
|
||||||
|
onRemove?: (file: File) => void;
|
||||||
|
multiple?: boolean;
|
||||||
|
maxFiles?: number;
|
||||||
|
maxSize?: number; // in bytes
|
||||||
|
accept?: Record<string, string[]>; // e.g. { 'image/*': ['.png', '.jpg'] }
|
||||||
|
showPreview?: boolean;
|
||||||
|
previewSize?: number;
|
||||||
|
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
activeClassName?: string;
|
||||||
|
acceptClassName?: string;
|
||||||
|
rejectClassName?: string;
|
||||||
|
|
||||||
|
uploadText?: string | { text: string; colorClass?: string }[];
|
||||||
|
dragActiveText?: string;
|
||||||
|
fileTypeText?: string;
|
||||||
|
sizeLimitText?: string;
|
||||||
|
|
||||||
|
disabled?: boolean;
|
||||||
|
validator?: (file: File) => { isValid: boolean; error?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileUpload = ({
|
||||||
|
onUpload,
|
||||||
|
onRemove,
|
||||||
|
multiple = false,
|
||||||
|
maxFiles = 1,
|
||||||
|
maxSize = 5 * 1024 * 1024,
|
||||||
|
accept = { 'image/*': ['.jpeg', '.png', '.jpg'] },
|
||||||
|
showPreview = false,
|
||||||
|
previewSize = 80,
|
||||||
|
children,
|
||||||
|
className = 'border-2 border-dashed rounded-3xl p-6 text-center cursor-pointer transition-colors border-silver dark:border-[#7E7E7E]',
|
||||||
|
activeClassName = 'border-blue-500 bg-blue-50',
|
||||||
|
acceptClassName = 'border-green-500 dark:border-green-500 bg-green-50 dark:bg-green-50/10',
|
||||||
|
rejectClassName = 'border-red-500 bg-red-50 dark:bg-red-500/10 dark:border-red-500',
|
||||||
|
uploadText = 'Click to upload or drag and drop',
|
||||||
|
dragActiveText = 'Drop the files here',
|
||||||
|
fileTypeText = 'PNG, JPG, JPEG up to',
|
||||||
|
sizeLimitText = 'MB',
|
||||||
|
disabled = false,
|
||||||
|
validator,
|
||||||
|
}: FileUploadProps) => {
|
||||||
|
const [errors, setErrors] = useState<string[]>([]);
|
||||||
|
const [preview, setPreview] = useState<string | null>(null);
|
||||||
|
const [currentFile, setCurrentFile] = useState<File | null>(null);
|
||||||
|
|
||||||
|
const validateFile = (file: File) => {
|
||||||
|
const defaultValidation = {
|
||||||
|
isValid: true,
|
||||||
|
error: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (validator) {
|
||||||
|
const customValidation = validator(file);
|
||||||
|
if (!customValidation.isValid) {
|
||||||
|
return customValidation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: `File exceeds ${maxSize / 1024 / 1024}MB limit`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValidation;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDrop = useCallback(
|
||||||
|
(acceptedFiles: File[], fileRejections: any[]) => {
|
||||||
|
setErrors([]);
|
||||||
|
|
||||||
|
if (fileRejections.length > 0) {
|
||||||
|
const newErrors = fileRejections
|
||||||
|
.map(({ errors }) => errors.map((e: any) => e.message))
|
||||||
|
.flat();
|
||||||
|
setErrors(newErrors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationResults = acceptedFiles.map(validateFile);
|
||||||
|
const invalidFiles = validationResults.filter((r) => !r.isValid);
|
||||||
|
|
||||||
|
if (invalidFiles.length > 0) {
|
||||||
|
setErrors(invalidFiles.map((f) => f.error!));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filesToUpload = multiple ? acceptedFiles : [acceptedFiles[0]];
|
||||||
|
onUpload(filesToUpload);
|
||||||
|
|
||||||
|
const file = multiple ? acceptedFiles[0] : acceptedFiles[0];
|
||||||
|
setCurrentFile(file);
|
||||||
|
|
||||||
|
if (showPreview && file.type.startsWith('image/')) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => setPreview(reader.result as string);
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onUpload, multiple, maxSize, validator],
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
getRootProps,
|
||||||
|
getInputProps,
|
||||||
|
isDragActive,
|
||||||
|
isDragAccept,
|
||||||
|
isDragReject,
|
||||||
|
} = useDropzone({
|
||||||
|
onDrop,
|
||||||
|
multiple,
|
||||||
|
maxFiles,
|
||||||
|
maxSize,
|
||||||
|
accept,
|
||||||
|
disabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentClassName = twMerge(
|
||||||
|
'border-2 border-dashed rounded-3xl p-8 text-center cursor-pointer transition-colors border-silver dark:border-[#7E7E7E]',
|
||||||
|
className,
|
||||||
|
isDragActive && activeClassName,
|
||||||
|
isDragAccept && acceptClassName,
|
||||||
|
isDragReject && rejectClassName,
|
||||||
|
disabled && 'opacity-50 cursor-not-allowed',
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemove = () => {
|
||||||
|
setPreview(null);
|
||||||
|
setCurrentFile(null);
|
||||||
|
if (onRemove && currentFile) onRemove(currentFile);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPreview = () => (
|
||||||
|
<div
|
||||||
|
className="relative"
|
||||||
|
style={{ width: previewSize, height: previewSize }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={preview ?? undefined}
|
||||||
|
alt="preview"
|
||||||
|
className="h-full w-full rounded-md object-cover"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRemove();
|
||||||
|
}}
|
||||||
|
className="absolute -right-2 -top-2 rounded-full bg-[#7D54D1] p-1 transition-colors hover:bg-[#714cbc]"
|
||||||
|
>
|
||||||
|
<img src={Cross} alt="remove" className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderUploadText = () => {
|
||||||
|
if (Array.isArray(uploadText)) {
|
||||||
|
return (
|
||||||
|
<p className="text-sm font-semibold">
|
||||||
|
{uploadText.map((segment, i) => (
|
||||||
|
<span key={i} className={segment.colorClass || ''}>
|
||||||
|
{segment.text}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <p className="text-sm font-semibold">{uploadText}</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultContent = (
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
{showPreview && preview ? (
|
||||||
|
renderPreview()
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{ width: previewSize, height: previewSize }}
|
||||||
|
className="flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<img src={ImagesIcon} className="h-10 w-10" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
{isDragActive ? (
|
||||||
|
<p className="text-sm font-semibold">{dragActiveText}</p>
|
||||||
|
) : (
|
||||||
|
renderUploadText()
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-[#A3A3A3]">
|
||||||
|
{fileTypeText} {maxSize / 1024 / 1024}
|
||||||
|
{sizeLimitText}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<div {...getRootProps({ className: currentClassName })}>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
{children || defaultContent}
|
||||||
|
{errors.length > 0 && (
|
||||||
|
<div className="absolute left-0 right-0 mt-[2px] px-4 text-xs text-red-600">
|
||||||
|
{errors.map((error, i) => (
|
||||||
|
<p key={i} className="truncate">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ import ClipIcon from '../assets/clip.svg';
|
|||||||
import ExitIcon from '../assets/exit.svg';
|
import ExitIcon from '../assets/exit.svg';
|
||||||
import PaperPlane from '../assets/paper_plane.svg';
|
import PaperPlane from '../assets/paper_plane.svg';
|
||||||
import SourceIcon from '../assets/source.svg';
|
import SourceIcon from '../assets/source.svg';
|
||||||
|
import DocumentationDark from '../assets/documentation-dark.svg';
|
||||||
import SpinnerDark from '../assets/spinner-dark.svg';
|
import SpinnerDark from '../assets/spinner-dark.svg';
|
||||||
import Spinner from '../assets/spinner.svg';
|
import Spinner from '../assets/spinner.svg';
|
||||||
import ToolIcon from '../assets/tool.svg';
|
import ToolIcon from '../assets/tool.svg';
|
||||||
@@ -17,7 +18,7 @@ import {
|
|||||||
removeAttachment,
|
removeAttachment,
|
||||||
selectAttachments,
|
selectAttachments,
|
||||||
updateAttachment,
|
updateAttachment,
|
||||||
} from '../conversation/conversationSlice';
|
} from '../upload/uploadSlice';
|
||||||
import { useDarkTheme } from '../hooks';
|
import { useDarkTheme } from '../hooks';
|
||||||
import { ActiveState } from '../models/misc';
|
import { ActiveState } from '../models/misc';
|
||||||
import {
|
import {
|
||||||
@@ -262,71 +263,81 @@ export default function MessageInput({
|
|||||||
{attachments.map((attachment, index) => (
|
{attachments.map((attachment, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
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] ${
|
className={`group relative flex items-center rounded-xl bg-[#EFF3F4] px-2 py-1 text-[12px] text-[#5D5D5D] dark:bg-[#393B3D] dark:text-bright-gray sm:px-3 sm:py-1.5 sm:text-[14px] ${
|
||||||
attachment.status !== 'completed' ? 'opacity-70' : 'opacity-100'
|
attachment.status !== 'completed' ? 'opacity-70' : 'opacity-100'
|
||||||
}`}
|
}`}
|
||||||
title={attachment.fileName}
|
title={attachment.fileName}
|
||||||
>
|
>
|
||||||
|
<div className="mr-2 items-center justify-center rounded-lg bg-purple-30 p-[5.5px]">
|
||||||
|
{attachment.status === 'completed' && (
|
||||||
|
<img
|
||||||
|
src={DocumentationDark}
|
||||||
|
alt="Attachment"
|
||||||
|
className="h-[15px] w-[15px] object-fill"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{attachment.status === 'failed' && (
|
||||||
|
<img
|
||||||
|
src={AlertIcon}
|
||||||
|
alt="Failed"
|
||||||
|
className="h-[15px] w-[15px] object-fill"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(attachment.status === 'uploading' ||
|
||||||
|
attachment.status === 'processing') && (
|
||||||
|
<div className="flex h-[15px] w-[15px] items-center justify-center">
|
||||||
|
<svg className="h-[15px] w-[15px]" viewBox="0 0 24 24">
|
||||||
|
<circle
|
||||||
|
className="opacity-0"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="transparent"
|
||||||
|
strokeWidth="4"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
className="text-[#ECECF1]"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
fill="none"
|
||||||
|
strokeDasharray="62.83"
|
||||||
|
strokeDashoffset={
|
||||||
|
62.83 * (1 - attachment.progress / 100)
|
||||||
|
}
|
||||||
|
transform="rotate(-90 12 12)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<span className="max-w-[120px] truncate font-medium sm:max-w-[150px]">
|
<span className="max-w-[120px] truncate font-medium sm:max-w-[150px]">
|
||||||
{attachment.fileName}
|
{attachment.fileName}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{attachment.status === 'completed' && (
|
<button
|
||||||
<button
|
className="ml-1.5 flex items-center justify-center rounded-full p-1"
|
||||||
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={() => {
|
||||||
onClick={() => {
|
if (attachment.id) {
|
||||||
if (attachment.id) {
|
dispatch(removeAttachment(attachment.id));
|
||||||
dispatch(removeAttachment(attachment.id));
|
} else if (attachment.taskId) {
|
||||||
}
|
dispatch(removeAttachment(attachment.taskId));
|
||||||
}}
|
}
|
||||||
aria-label={t('conversation.attachments.remove')}
|
}}
|
||||||
>
|
aria-label={t('conversation.attachments.remove')}
|
||||||
<img
|
>
|
||||||
src={ExitIcon}
|
|
||||||
alt={t('conversation.attachments.remove')}
|
|
||||||
className="h-2.5 w-2.5 filter dark:invert"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{attachment.status === 'failed' && (
|
|
||||||
<img
|
<img
|
||||||
src={AlertIcon}
|
src={ExitIcon}
|
||||||
alt="Upload failed"
|
alt={t('conversation.attachments.remove')}
|
||||||
className="ml-2 h-3.5 w-3.5"
|
className="h-2.5 w-2.5 filter dark:invert"
|
||||||
title="Upload failed"
|
|
||||||
/>
|
/>
|
||||||
)}
|
</button>
|
||||||
|
|
||||||
{(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"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="4"
|
|
||||||
fill="none"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
className="text-blue-600 dark:text-blue-400"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="4"
|
|
||||||
fill="none"
|
|
||||||
strokeDasharray="62.83"
|
|
||||||
strokeDashoffset={62.83 * (1 - attachment.progress / 100)}
|
|
||||||
transform="rotate(-90 12 12)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ import {
|
|||||||
updateConversationId,
|
updateConversationId,
|
||||||
updateQuery,
|
updateQuery,
|
||||||
} from './conversationSlice';
|
} from './conversationSlice';
|
||||||
|
import {
|
||||||
|
selectCompletedAttachments,
|
||||||
|
clearAttachments,
|
||||||
|
} from '../upload/uploadSlice';
|
||||||
|
|
||||||
export default function Conversation() {
|
export default function Conversation() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -39,6 +43,7 @@ export default function Conversation() {
|
|||||||
const status = useSelector(selectStatus);
|
const status = useSelector(selectStatus);
|
||||||
const conversationId = useSelector(selectConversationId);
|
const conversationId = useSelector(selectConversationId);
|
||||||
const selectedAgent = useSelector(selectSelectedAgent);
|
const selectedAgent = useSelector(selectSelectedAgent);
|
||||||
|
const completedAttachments = useSelector(selectCompletedAttachments);
|
||||||
|
|
||||||
const [uploadModalState, setUploadModalState] =
|
const [uploadModalState, setUploadModalState] =
|
||||||
useState<ActiveState>('INACTIVE');
|
useState<ActiveState>('INACTIVE');
|
||||||
@@ -107,15 +112,25 @@ export default function Conversation() {
|
|||||||
const trimmedQuestion = question.trim();
|
const trimmedQuestion = question.trim();
|
||||||
if (trimmedQuestion === '') return;
|
if (trimmedQuestion === '') return;
|
||||||
|
|
||||||
|
const filesAttached = completedAttachments
|
||||||
|
.filter((a) => a.id)
|
||||||
|
.map((a) => ({ id: a.id as string, fileName: a.fileName }));
|
||||||
|
|
||||||
if (index !== undefined) {
|
if (index !== undefined) {
|
||||||
if (!isRetry) dispatch(resendQuery({ index, prompt: trimmedQuestion }));
|
if (!isRetry) dispatch(resendQuery({ index, prompt: trimmedQuestion }));
|
||||||
handleFetchAnswer({ question: trimmedQuestion, index });
|
handleFetchAnswer({ question: trimmedQuestion, index });
|
||||||
} else {
|
} else {
|
||||||
if (!isRetry) dispatch(addQuery({ prompt: trimmedQuestion }));
|
if (!isRetry)
|
||||||
|
dispatch(
|
||||||
|
addQuery({
|
||||||
|
prompt: trimmedQuestion,
|
||||||
|
attachments: filesAttached,
|
||||||
|
}),
|
||||||
|
);
|
||||||
handleFetchAnswer({ question: trimmedQuestion, index });
|
handleFetchAnswer({ question: trimmedQuestion, index });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch, handleFetchAnswer],
|
[dispatch, handleFetchAnswer, completedAttachments],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => {
|
const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => {
|
||||||
@@ -178,6 +193,7 @@ export default function Conversation() {
|
|||||||
query: { conversationId: null },
|
query: { conversationId: null },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
dispatch(clearAttachments());
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'katex/dist/katex.min.css';
|
import 'katex/dist/katex.min.css';
|
||||||
|
|
||||||
import { forwardRef, Fragment, useRef, useState } from 'react';
|
import { forwardRef, Fragment, useRef, useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import rehypeKatex from 'rehype-katex';
|
import rehypeKatex from 'rehype-katex';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import remarkMath from 'remark-math';
|
import remarkMath from 'remark-math';
|
||||||
|
import DocumentationDark from '../assets/documentation-dark.svg';
|
||||||
import ChevronDown from '../assets/chevron-down.svg';
|
import ChevronDown from '../assets/chevron-down.svg';
|
||||||
import Cloud from '../assets/cloud.svg';
|
import Cloud from '../assets/cloud.svg';
|
||||||
import DocsGPT3 from '../assets/cute_docsgpt3.svg';
|
import DocsGPT3 from '../assets/cute_docsgpt3.svg';
|
||||||
@@ -26,7 +26,9 @@ import UserIcon from '../assets/user.svg';
|
|||||||
import Accordion from '../components/Accordion';
|
import Accordion from '../components/Accordion';
|
||||||
import Avatar from '../components/Avatar';
|
import Avatar from '../components/Avatar';
|
||||||
import CopyButton from '../components/CopyButton';
|
import CopyButton from '../components/CopyButton';
|
||||||
|
import MermaidRenderer from '../components/MermaidRenderer';
|
||||||
import Sidebar from '../components/Sidebar';
|
import Sidebar from '../components/Sidebar';
|
||||||
|
import Spinner from '../components/Spinner';
|
||||||
import SpeakButton from '../components/TextToSpeechButton';
|
import SpeakButton from '../components/TextToSpeechButton';
|
||||||
import { useDarkTheme, useOutsideAlerter } from '../hooks';
|
import { useDarkTheme, useOutsideAlerter } from '../hooks';
|
||||||
import {
|
import {
|
||||||
@@ -36,7 +38,6 @@ import {
|
|||||||
import classes from './ConversationBubble.module.css';
|
import classes from './ConversationBubble.module.css';
|
||||||
import { FEEDBACK, MESSAGE_TYPE } from './conversationModels';
|
import { FEEDBACK, MESSAGE_TYPE } from './conversationModels';
|
||||||
import { ToolCallsType } from './types';
|
import { ToolCallsType } from './types';
|
||||||
import MermaidRenderer from '../components/MermaidRenderer';
|
|
||||||
|
|
||||||
const DisableSourceFE = import.meta.env.VITE_DISABLE_SOURCE_FE || false;
|
const DisableSourceFE = import.meta.env.VITE_DISABLE_SOURCE_FE || false;
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ const ConversationBubble = forwardRef<
|
|||||||
updated?: boolean,
|
updated?: boolean,
|
||||||
index?: number,
|
index?: number,
|
||||||
) => void;
|
) => void;
|
||||||
|
filesAttached?: { id: string; fileName: string }[];
|
||||||
}
|
}
|
||||||
>(function ConversationBubble(
|
>(function ConversationBubble(
|
||||||
{
|
{
|
||||||
@@ -74,6 +76,7 @@ const ConversationBubble = forwardRef<
|
|||||||
questionNumber,
|
questionNumber,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
handleUpdatedQuestionSubmission,
|
handleUpdatedQuestionSubmission,
|
||||||
|
filesAttached,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
@@ -87,14 +90,24 @@ const ConversationBubble = forwardRef<
|
|||||||
const [isDislikeHovered, setIsDislikeHovered] = useState(false);
|
const [isDislikeHovered, setIsDislikeHovered] = useState(false);
|
||||||
const [isQuestionHovered, setIsQuestionHovered] = useState(false);
|
const [isQuestionHovered, setIsQuestionHovered] = useState(false);
|
||||||
const [editInputBox, setEditInputBox] = useState<string>('');
|
const [editInputBox, setEditInputBox] = useState<string>('');
|
||||||
|
const messageRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [shouldShowToggle, setShouldShowToggle] = useState(false);
|
||||||
const [isLikeClicked, setIsLikeClicked] = useState(false);
|
const [isLikeClicked, setIsLikeClicked] = useState(false);
|
||||||
const [isDislikeClicked, setIsDislikeClicked] = useState(false);
|
const [isDislikeClicked, setIsDislikeClicked] = useState(false);
|
||||||
const [activeTooltip, setActiveTooltip] = useState<number | null>(null);
|
const [activeTooltip, setActiveTooltip] = useState<number | null>(null);
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false);
|
||||||
const editableQueryRef = useRef<HTMLDivElement | null>(null);
|
const editableQueryRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [isQuestionCollapsed, setIsQuestionCollapsed] = useState(true);
|
||||||
|
|
||||||
useOutsideAlerter(editableQueryRef, () => setIsEditClicked(false), [], true);
|
useOutsideAlerter(editableQueryRef, () => setIsEditClicked(false), [], true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (messageRef.current) {
|
||||||
|
const height = messageRef.current.scrollHeight;
|
||||||
|
setShouldShowToggle(height > 84);
|
||||||
|
}
|
||||||
|
}, [message]);
|
||||||
|
|
||||||
const handleEditClick = () => {
|
const handleEditClick = () => {
|
||||||
setIsEditClicked(false);
|
setIsEditClicked(false);
|
||||||
handleUpdatedQuestionSubmission?.(editInputBox, true, questionNumber);
|
handleUpdatedQuestionSubmission?.(editInputBox, true, questionNumber);
|
||||||
@@ -105,41 +118,88 @@ const ConversationBubble = forwardRef<
|
|||||||
<div
|
<div
|
||||||
onMouseEnter={() => setIsQuestionHovered(true)}
|
onMouseEnter={() => setIsQuestionHovered(true)}
|
||||||
onMouseLeave={() => setIsQuestionHovered(false)}
|
onMouseLeave={() => setIsQuestionHovered(false)}
|
||||||
|
className={className}
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex flex-col items-end">
|
||||||
ref={ref}
|
{filesAttached && filesAttached.length > 0 && (
|
||||||
className={`flex flex-row-reverse justify-items-start ${className}`}
|
<div className="mb-4 mr-12 flex flex-wrap justify-end gap-2">
|
||||||
>
|
{filesAttached.map((file, index) => (
|
||||||
<Avatar
|
|
||||||
size="SMALL"
|
|
||||||
className="mt-2 flex-shrink-0 text-2xl"
|
|
||||||
avatar={
|
|
||||||
<img className="mr-1 rounded-full" width={30} src={UserIcon} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{!isEditClicked && (
|
|
||||||
<>
|
|
||||||
<div className="mr-2 flex flex-col">
|
|
||||||
<div
|
<div
|
||||||
style={{
|
key={index}
|
||||||
wordBreak: 'break-word',
|
title={file.fileName}
|
||||||
}}
|
className="flex items-center rounded-xl bg-[#EFF3F4] p-2 text-[14px] text-[#5D5D5D] dark:bg-[#393B3D] dark:text-bright-gray"
|
||||||
className="ml-2 mr-2 flex max-w-full items-center whitespace-pre-wrap rounded-[28px] bg-gradient-to-b from-medium-purple to-slate-blue px-[19px] py-[14px] text-sm leading-normal text-white sm:text-base"
|
|
||||||
>
|
>
|
||||||
{message}
|
<div className="mr-2 items-center justify-center rounded-lg bg-purple-30 p-[5.5px]">
|
||||||
|
<img
|
||||||
|
src={DocumentationDark}
|
||||||
|
alt="Attachment"
|
||||||
|
className="h-[15px] w-[15px] object-fill"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="max-w-[150px] truncate font-normal">
|
||||||
|
{file.fileName}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
<button
|
</div>
|
||||||
onClick={() => {
|
|
||||||
setIsEditClicked(true);
|
|
||||||
setEditInputBox(message ?? '');
|
|
||||||
}}
|
|
||||||
className={`mt-3 flex h-fit flex-shrink-0 cursor-pointer items-center rounded-full p-2 hover:bg-light-silver dark:hover:bg-[#35363B] ${isQuestionHovered || isEditClicked ? 'visible' : 'invisible'}`}
|
|
||||||
>
|
|
||||||
<img src={Edit} alt="Edit" className="cursor-pointer" />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={`flex flex-row-reverse justify-items-start`}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
size="SMALL"
|
||||||
|
className="mt-2 flex-shrink-0 text-2xl"
|
||||||
|
avatar={
|
||||||
|
<img className="mr-1 rounded-full" width={30} src={UserIcon} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{!isEditClicked && (
|
||||||
|
<>
|
||||||
|
<div className="relative mr-2 flex w-full flex-col">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
}}
|
||||||
|
className="ml-2 mr-2 flex max-w-full items-start gap-2 whitespace-pre-wrap rounded-[28px] bg-gradient-to-b from-medium-purple to-slate-blue py-[14px] pl-[19px] pr-3 text-sm leading-normal text-white sm:text-base"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={messageRef}
|
||||||
|
className={`${isQuestionCollapsed ? 'line-clamp-4' : ''} w-full`}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
{shouldShowToggle && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsQuestionCollapsed(!isQuestionCollapsed);
|
||||||
|
}}
|
||||||
|
className="rounded-full p-2.5 hover:bg-[#D9D9D933]"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={ChevronDown}
|
||||||
|
alt="Toggle"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className={`transform invert transition-transform duration-200 ${isQuestionCollapsed ? '' : 'rotate-180'}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditClicked(true);
|
||||||
|
setEditInputBox(message ?? '');
|
||||||
|
}}
|
||||||
|
className={`mt-3 flex h-fit flex-shrink-0 cursor-pointer items-center rounded-full p-2 hover:bg-light-silver dark:hover:bg-[#35363B] ${isQuestionHovered || isEditClicked ? 'visible' : 'invisible'}`}
|
||||||
|
>
|
||||||
|
<img src={Edit} alt="Edit" className="cursor-pointer" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{isEditClicked && (
|
{isEditClicked && (
|
||||||
<div
|
<div
|
||||||
ref={editableQueryRef}
|
ref={editableQueryRef}
|
||||||
@@ -741,7 +801,7 @@ function ToolCalls({ toolCalls }: { toolCalls: ToolCallsType[] }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{isToolCallsOpen && (
|
{isToolCallsOpen && (
|
||||||
<div className="fade-in ml-3 mr-5 max-w-[90vw] md:max-w-[70vw] lg:max-w-[50vw]">
|
<div className="fade-in ml-3 mr-5 w-[90vw] md:w-[70vw] lg:w-full">
|
||||||
<div className="grid grid-cols-1 gap-2">
|
<div className="grid grid-cols-1 gap-2">
|
||||||
{toolCalls.map((toolCall, index) => (
|
{toolCalls.map((toolCall, index) => (
|
||||||
<Accordion
|
<Accordion
|
||||||
@@ -778,14 +838,21 @@ function ToolCalls({ toolCalls }: { toolCalls: ToolCallsType[] }) {
|
|||||||
textToCopy={JSON.stringify(toolCall.result, null, 2)}
|
textToCopy={JSON.stringify(toolCall.result, null, 2)}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p className="dark:tex break-words rounded-b-2xl p-2 font-mono text-sm dark:bg-[#222327]">
|
{toolCall.status === 'pending' && (
|
||||||
<span
|
<span className="flex w-full items-center justify-center rounded-b-2xl p-2 dark:bg-[#222327]">
|
||||||
className="leading-[23px] text-black dark:text-gray-400"
|
<Spinner size="small" />
|
||||||
style={{ fontFamily: 'IBMPlexMono-Medium' }}
|
|
||||||
>
|
|
||||||
{JSON.stringify(toolCall.result, null, 2)}
|
|
||||||
</span>
|
</span>
|
||||||
</p>
|
)}
|
||||||
|
{toolCall.status === 'completed' && (
|
||||||
|
<p className="break-words rounded-b-2xl p-2 font-mono text-sm dark:bg-[#222327]">
|
||||||
|
<span
|
||||||
|
className="leading-[23px] text-black dark:text-gray-400"
|
||||||
|
style={{ fontFamily: 'IBMPlexMono-Medium' }}
|
||||||
|
>
|
||||||
|
{JSON.stringify(toolCall.result, null, 2)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export default function ConversationMessages({
|
|||||||
? LAST_BUBBLE_MARGIN
|
? LAST_BUBBLE_MARGIN
|
||||||
: DEFAULT_BUBBLE_MARGIN;
|
: DEFAULT_BUBBLE_MARGIN;
|
||||||
|
|
||||||
if (query.thought || query.response) {
|
if (query.thought || query.response || query.tool_calls) {
|
||||||
const isCurrentlyStreaming =
|
const isCurrentlyStreaming =
|
||||||
status === 'loading' && index === queries.length - 1;
|
status === 'loading' && index === queries.length - 1;
|
||||||
return (
|
return (
|
||||||
@@ -223,6 +223,7 @@ export default function ConversationMessages({
|
|||||||
handleUpdatedQuestionSubmission={handleQuestionSubmission}
|
handleUpdatedQuestionSubmission={handleQuestionSubmission}
|
||||||
questionNumber={index}
|
questionNumber={index}
|
||||||
sources={query.sources}
|
sources={query.sources}
|
||||||
|
filesAttached={query.attachments}
|
||||||
/>
|
/>
|
||||||
{renderResponseView(query, index)}
|
{renderResponseView(query, index)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
setIdentifier,
|
setIdentifier,
|
||||||
updateQuery,
|
updateQuery,
|
||||||
} from './sharedConversationSlice';
|
} from './sharedConversationSlice';
|
||||||
|
import { selectCompletedAttachments } from '../upload/uploadSlice';
|
||||||
|
|
||||||
export const SharedConversation = () => {
|
export const SharedConversation = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -34,6 +35,7 @@ export const SharedConversation = () => {
|
|||||||
const date = useSelector(selectDate);
|
const date = useSelector(selectDate);
|
||||||
const apiKey = useSelector(selectClientAPIKey);
|
const apiKey = useSelector(selectClientAPIKey);
|
||||||
const status = useSelector(selectStatus);
|
const status = useSelector(selectStatus);
|
||||||
|
const completedAttachments = useSelector(selectCompletedAttachments);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
@@ -106,7 +108,19 @@ export const SharedConversation = () => {
|
|||||||
}) => {
|
}) => {
|
||||||
question = question.trim();
|
question = question.trim();
|
||||||
if (question === '') return;
|
if (question === '') return;
|
||||||
!isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries
|
|
||||||
|
const filesAttached = completedAttachments
|
||||||
|
.filter((a) => a.id)
|
||||||
|
.map((a) => ({ id: a.id as string, fileName: a.fileName }));
|
||||||
|
|
||||||
|
!isRetry &&
|
||||||
|
dispatch(
|
||||||
|
addQuery({
|
||||||
|
prompt: question,
|
||||||
|
attachments: filesAttached,
|
||||||
|
}),
|
||||||
|
); //dispatch only new queries
|
||||||
|
|
||||||
dispatch(fetchSharedAnswer({ question }));
|
dispatch(fetchSharedAnswer({ question }));
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -279,11 +279,12 @@ export function handleSendFeedback(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleFetchSharedAnswerStreaming( //for shared conversations
|
export function handleFetchSharedAnswerStreaming(
|
||||||
question: string,
|
question: string,
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
history: Array<any> = [],
|
history: Array<any> = [],
|
||||||
|
attachments: string[] = [],
|
||||||
onEvent: (event: MessageEvent) => void,
|
onEvent: (event: MessageEvent) => void,
|
||||||
): Promise<Answer> {
|
): Promise<Answer> {
|
||||||
history = history.map((item) => {
|
history = history.map((item) => {
|
||||||
@@ -300,6 +301,7 @@ export function handleFetchSharedAnswerStreaming( //for shared conversations
|
|||||||
history: JSON.stringify(history),
|
history: JSON.stringify(history),
|
||||||
api_key: apiKey,
|
api_key: apiKey,
|
||||||
save_conversation: false,
|
save_conversation: false,
|
||||||
|
attachments: attachments.length > 0 ? attachments : undefined,
|
||||||
};
|
};
|
||||||
conversationService
|
conversationService
|
||||||
.answerStream(payload, null, signal)
|
.answerStream(payload, null, signal)
|
||||||
@@ -355,6 +357,7 @@ export function handleFetchSharedAnswer(
|
|||||||
question: string,
|
question: string,
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
|
attachments?: string[],
|
||||||
): Promise<
|
): Promise<
|
||||||
| {
|
| {
|
||||||
result: any;
|
result: any;
|
||||||
@@ -370,15 +373,15 @@ export function handleFetchSharedAnswer(
|
|||||||
title: any;
|
title: any;
|
||||||
}
|
}
|
||||||
> {
|
> {
|
||||||
|
const payload = {
|
||||||
|
question: question,
|
||||||
|
api_key: apiKey,
|
||||||
|
attachments:
|
||||||
|
attachments && attachments.length > 0 ? attachments : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
return conversationService
|
return conversationService
|
||||||
.answer(
|
.answer(payload, null, signal)
|
||||||
{
|
|
||||||
question: question,
|
|
||||||
api_key: apiKey,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
signal,
|
|
||||||
)
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return response.json();
|
return response.json();
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ export interface ConversationState {
|
|||||||
queries: Query[];
|
queries: Query[];
|
||||||
status: Status;
|
status: Status;
|
||||||
conversationId: string | null;
|
conversationId: string | null;
|
||||||
attachments: Attachment[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Answer {
|
export interface Answer {
|
||||||
@@ -46,7 +45,7 @@ export interface Query {
|
|||||||
sources?: { title: string; text: string; link: string }[];
|
sources?: { title: string; text: string; link: string }[];
|
||||||
tool_calls?: ToolCallsType[];
|
tool_calls?: ToolCallsType[];
|
||||||
error?: string;
|
error?: string;
|
||||||
attachments?: { fileName: string; id: string }[];
|
attachments?: { id: string; fileName: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RetrievalPayload {
|
export interface RetrievalPayload {
|
||||||
|
|||||||
@@ -3,23 +3,27 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|||||||
import { getConversations } from '../preferences/preferenceApi';
|
import { getConversations } from '../preferences/preferenceApi';
|
||||||
import { setConversations } from '../preferences/preferenceSlice';
|
import { setConversations } from '../preferences/preferenceSlice';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
|
import {
|
||||||
|
clearAttachments,
|
||||||
|
selectCompletedAttachments,
|
||||||
|
} from '../upload/uploadSlice';
|
||||||
import {
|
import {
|
||||||
handleFetchAnswer,
|
handleFetchAnswer,
|
||||||
handleFetchAnswerSteaming,
|
handleFetchAnswerSteaming,
|
||||||
} from './conversationHandlers';
|
} from './conversationHandlers';
|
||||||
import {
|
import {
|
||||||
Answer,
|
Answer,
|
||||||
|
Attachment,
|
||||||
|
ConversationState,
|
||||||
Query,
|
Query,
|
||||||
Status,
|
Status,
|
||||||
ConversationState,
|
|
||||||
Attachment,
|
|
||||||
} from './conversationModels';
|
} from './conversationModels';
|
||||||
|
import { ToolCallsType } from './types';
|
||||||
|
|
||||||
const initialState: ConversationState = {
|
const initialState: ConversationState = {
|
||||||
queries: [],
|
queries: [],
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
conversationId: null,
|
conversationId: null,
|
||||||
attachments: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
|
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
|
||||||
@@ -44,9 +48,14 @@ export const fetchAnswer = createAsyncThunk<
|
|||||||
|
|
||||||
let isSourceUpdated = false;
|
let isSourceUpdated = false;
|
||||||
const state = getState() as RootState;
|
const state = getState() as RootState;
|
||||||
const attachmentIds = state.conversation.attachments
|
const attachmentIds = selectCompletedAttachments(state)
|
||||||
.filter((a) => a.id && a.status === 'completed')
|
.filter((a) => a.id)
|
||||||
.map((a) => a.id) as string[];
|
.map((a) => a.id) as string[];
|
||||||
|
|
||||||
|
if (attachmentIds.length > 0) {
|
||||||
|
dispatch(clearAttachments());
|
||||||
|
}
|
||||||
|
|
||||||
const currentConversationId = state.conversation.conversationId;
|
const currentConversationId = state.conversation.conversationId;
|
||||||
const conversationIdToSend = isPreview ? null : currentConversationId;
|
const conversationIdToSend = isPreview ? null : currentConversationId;
|
||||||
const save_conversation = isPreview ? false : true;
|
const save_conversation = isPreview ? false : true;
|
||||||
@@ -110,11 +119,11 @@ export const fetchAnswer = createAsyncThunk<
|
|||||||
query: { sources: data.source ?? [] },
|
query: { sources: data.source ?? [] },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else if (data.type === 'tool_calls') {
|
} else if (data.type === 'tool_call') {
|
||||||
dispatch(
|
dispatch(
|
||||||
updateToolCalls({
|
updateToolCall({
|
||||||
index: targetIndex,
|
index: targetIndex,
|
||||||
query: { tool_calls: data.tool_calls },
|
tool_call: data.data as ToolCallsType,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else if (data.type === 'error') {
|
} else if (data.type === 'error') {
|
||||||
@@ -280,12 +289,24 @@ export const conversationSlice = createSlice({
|
|||||||
state.queries[index].sources!.push(query.sources![0]);
|
state.queries[index].sources!.push(query.sources![0]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateToolCalls(
|
updateToolCall(state, action) {
|
||||||
state,
|
const { index, tool_call } = action.payload;
|
||||||
action: PayloadAction<{ index: number; query: Partial<Query> }>,
|
|
||||||
) {
|
if (!state.queries[index].tool_calls) {
|
||||||
const { index, query } = action.payload;
|
state.queries[index].tool_calls = [];
|
||||||
state.queries[index].tool_calls = query?.tool_calls ?? [];
|
}
|
||||||
|
|
||||||
|
const existingIndex = state.queries[index].tool_calls.findIndex(
|
||||||
|
(call) => call.call_id === tool_call.call_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
const existingCall = state.queries[index].tool_calls[existingIndex];
|
||||||
|
state.queries[index].tool_calls[existingIndex] = {
|
||||||
|
...existingCall,
|
||||||
|
...tool_call,
|
||||||
|
};
|
||||||
|
} else state.queries[index].tool_calls.push(tool_call);
|
||||||
},
|
},
|
||||||
updateQuery(
|
updateQuery(
|
||||||
state,
|
state,
|
||||||
@@ -307,39 +328,11 @@ export const conversationSlice = createSlice({
|
|||||||
const { index, message } = action.payload;
|
const { index, message } = action.payload;
|
||||||
state.queries[index].error = message;
|
state.queries[index].error = message;
|
||||||
},
|
},
|
||||||
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) => {
|
resetConversation: (state) => {
|
||||||
state.queries = initialState.queries;
|
state.queries = initialState.queries;
|
||||||
state.status = initialState.status;
|
state.status = initialState.status;
|
||||||
state.conversationId = initialState.conversationId;
|
state.conversationId = initialState.conversationId;
|
||||||
state.attachments = initialState.attachments;
|
|
||||||
handleAbort();
|
handleAbort();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -365,11 +358,6 @@ export const selectQueries = (state: RootState) => state.conversation.queries;
|
|||||||
|
|
||||||
export const selectStatus = (state: RootState) => state.conversation.status;
|
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 {
|
export const {
|
||||||
addQuery,
|
addQuery,
|
||||||
updateQuery,
|
updateQuery,
|
||||||
@@ -378,12 +366,10 @@ export const {
|
|||||||
updateConversationId,
|
updateConversationId,
|
||||||
updateThought,
|
updateThought,
|
||||||
updateStreamingSource,
|
updateStreamingSource,
|
||||||
updateToolCalls,
|
updateToolCall,
|
||||||
setConversation,
|
setConversation,
|
||||||
setAttachments,
|
setStatus,
|
||||||
addAttachment,
|
raiseError,
|
||||||
updateAttachment,
|
|
||||||
removeAttachment,
|
|
||||||
resetConversation,
|
resetConversation,
|
||||||
} = conversationSlice.actions;
|
} = conversationSlice.actions;
|
||||||
export default conversationSlice.reducer;
|
export default conversationSlice.reducer;
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import {
|
|||||||
handleFetchSharedAnswer,
|
handleFetchSharedAnswer,
|
||||||
handleFetchSharedAnswerStreaming,
|
handleFetchSharedAnswerStreaming,
|
||||||
} from './conversationHandlers';
|
} from './conversationHandlers';
|
||||||
|
import {
|
||||||
|
selectCompletedAttachments,
|
||||||
|
clearAttachments,
|
||||||
|
} from '../upload/uploadSlice';
|
||||||
|
|
||||||
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
|
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
|
||||||
interface SharedConversationsType {
|
interface SharedConversationsType {
|
||||||
@@ -29,6 +33,14 @@ export const fetchSharedAnswer = createAsyncThunk<Answer, { question: string }>(
|
|||||||
async ({ question }, { dispatch, getState, signal }) => {
|
async ({ question }, { dispatch, getState, signal }) => {
|
||||||
const state = getState() as RootState;
|
const state = getState() as RootState;
|
||||||
|
|
||||||
|
const attachmentIds = selectCompletedAttachments(state)
|
||||||
|
.filter((a) => a.id)
|
||||||
|
.map((a) => a.id) as string[];
|
||||||
|
|
||||||
|
if (attachmentIds.length > 0) {
|
||||||
|
dispatch(clearAttachments());
|
||||||
|
}
|
||||||
|
|
||||||
if (state.preference && state.sharedConversation.apiKey) {
|
if (state.preference && state.sharedConversation.apiKey) {
|
||||||
if (API_STREAMING) {
|
if (API_STREAMING) {
|
||||||
await handleFetchSharedAnswerStreaming(
|
await handleFetchSharedAnswerStreaming(
|
||||||
@@ -36,7 +48,7 @@ export const fetchSharedAnswer = createAsyncThunk<Answer, { question: string }>(
|
|||||||
signal,
|
signal,
|
||||||
state.sharedConversation.apiKey,
|
state.sharedConversation.apiKey,
|
||||||
state.sharedConversation.queries,
|
state.sharedConversation.queries,
|
||||||
|
attachmentIds,
|
||||||
(event) => {
|
(event) => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
// check if the 'end' event has been received
|
// check if the 'end' event has been received
|
||||||
@@ -92,6 +104,7 @@ export const fetchSharedAnswer = createAsyncThunk<Answer, { question: string }>(
|
|||||||
question,
|
question,
|
||||||
signal,
|
signal,
|
||||||
state.sharedConversation.apiKey,
|
state.sharedConversation.apiKey,
|
||||||
|
attachmentIds,
|
||||||
);
|
);
|
||||||
if (answer) {
|
if (answer) {
|
||||||
let sourcesPrepped = [];
|
let sourcesPrepped = [];
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ export type ToolCallsType = {
|
|||||||
action_name: string;
|
action_name: string;
|
||||||
call_id: string;
|
call_id: string;
|
||||||
arguments: Record<string, any>;
|
arguments: Record<string, any>;
|
||||||
result: Record<string, any>;
|
result?: Record<string, any>;
|
||||||
|
status?: 'pending' | 'completed';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap');
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|||||||
@@ -245,7 +245,8 @@
|
|||||||
"promptName": "Prompt Name",
|
"promptName": "Prompt Name",
|
||||||
"promptText": "Prompt Text",
|
"promptText": "Prompt Text",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"nameExists": "Name already exists"
|
"nameExists": "Name already exists",
|
||||||
|
"deleteConfirmation": "Are you sure you want to delete the prompt '{{name}}'?"
|
||||||
},
|
},
|
||||||
"chunk": {
|
"chunk": {
|
||||||
"add": "Add Chunk",
|
"add": "Add Chunk",
|
||||||
|
|||||||
@@ -245,7 +245,8 @@
|
|||||||
"promptName": "Nombre del Prompt",
|
"promptName": "Nombre del Prompt",
|
||||||
"promptText": "Texto del Prompt",
|
"promptText": "Texto del Prompt",
|
||||||
"save": "Guardar",
|
"save": "Guardar",
|
||||||
"nameExists": "El nombre ya existe"
|
"nameExists": "El nombre ya existe",
|
||||||
|
"deleteConfirmation": "¿Estás seguro de que deseas eliminar el prompt '{{name}}'?"
|
||||||
},
|
},
|
||||||
"chunk": {
|
"chunk": {
|
||||||
"add": "Agregar Fragmento",
|
"add": "Agregar Fragmento",
|
||||||
|
|||||||
@@ -245,7 +245,8 @@
|
|||||||
"promptName": "プロンプト名",
|
"promptName": "プロンプト名",
|
||||||
"promptText": "プロンプトテキスト",
|
"promptText": "プロンプトテキスト",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"nameExists": "名前が既に存在します"
|
"nameExists": "名前が既に存在します",
|
||||||
|
"deleteConfirmation": "プロンプト「{{name}}」を削除してもよろしいですか?"
|
||||||
},
|
},
|
||||||
"chunk": {
|
"chunk": {
|
||||||
"add": "チャンクを追加",
|
"add": "チャンクを追加",
|
||||||
|
|||||||
@@ -245,7 +245,8 @@
|
|||||||
"promptName": "Название подсказки",
|
"promptName": "Название подсказки",
|
||||||
"promptText": "Текст подсказки",
|
"promptText": "Текст подсказки",
|
||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
"nameExists": "Название уже существует"
|
"nameExists": "Название уже существует",
|
||||||
|
"deleteConfirmation": "Вы уверены, что хотите удалить подсказку «{{name}}»?"
|
||||||
},
|
},
|
||||||
"chunk": {
|
"chunk": {
|
||||||
"add": "Добавить фрагмент",
|
"add": "Добавить фрагмент",
|
||||||
|
|||||||
@@ -245,7 +245,8 @@
|
|||||||
"promptName": "提示名稱",
|
"promptName": "提示名稱",
|
||||||
"promptText": "提示文字",
|
"promptText": "提示文字",
|
||||||
"save": "儲存",
|
"save": "儲存",
|
||||||
"nameExists": "名稱已存在"
|
"nameExists": "名稱已存在",
|
||||||
|
"deleteConfirmation": "您確定要刪除提示「{{name}}」嗎?"
|
||||||
},
|
},
|
||||||
"chunk": {
|
"chunk": {
|
||||||
"add": "新增區塊",
|
"add": "新增區塊",
|
||||||
|
|||||||
@@ -245,7 +245,8 @@
|
|||||||
"promptName": "提示名称",
|
"promptName": "提示名称",
|
||||||
"promptText": "提示文本",
|
"promptText": "提示文本",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"nameExists": "名称已存在"
|
"nameExists": "名称已存在",
|
||||||
|
"deleteConfirmation": "您确定要删除提示'{{name}}'吗?"
|
||||||
},
|
},
|
||||||
"chunk": {
|
"chunk": {
|
||||||
"add": "添加块",
|
"add": "添加块",
|
||||||
|
|||||||
@@ -94,24 +94,40 @@ export default function AgentDetailsModal({
|
|||||||
<h2 className="text-base font-semibold text-jet dark:text-bright-gray">
|
<h2 className="text-base font-semibold text-jet dark:text-bright-gray">
|
||||||
Public Link
|
Public Link
|
||||||
</h2>
|
</h2>
|
||||||
{sharedToken && (
|
</div>
|
||||||
<div className="mb-1">
|
{sharedToken ? (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<p className="inline break-all font-roboto text-[14px] font-medium leading-normal text-gray-700 dark:text-[#ECECF1]">
|
||||||
|
<a
|
||||||
|
href={`${baseURL}/shared/agent/${sharedToken}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{`${baseURL}/shared/agent/${sharedToken}`}
|
||||||
|
</a>
|
||||||
<CopyButton
|
<CopyButton
|
||||||
textToCopy={`${baseURL}/shared/agent/${sharedToken}`}
|
textToCopy={`${baseURL}/shared/agent/${sharedToken}`}
|
||||||
padding="p-1"
|
padding="p-1"
|
||||||
|
className="absolute -mt-0.5 ml-1 inline-flex"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{sharedToken ? (
|
|
||||||
<div className="flex flex-col flex-wrap items-start gap-2">
|
|
||||||
<p className="f break-all font-mono text-sm text-gray-700 dark:text-[#ECECF1]">
|
|
||||||
{`${baseURL}/shared/agent/${sharedToken}`}
|
|
||||||
</p>
|
</p>
|
||||||
|
<a
|
||||||
|
href="https://docs.docsgpt.cloud/Agents/basics#core-components-of-an-agent"
|
||||||
|
className="flex w-fit items-center gap-1 text-purple-30 hover:underline"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<span className="text-sm">Learn more</span>
|
||||||
|
<img
|
||||||
|
src="/src/assets/external-link.svg"
|
||||||
|
alt="External link"
|
||||||
|
className="h-3 w-3"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
className="hover:bg-vi</button>olets-are-blue flex w-28 items-center justify-center rounded-3xl border border-solid border-violets-are-blue px-5 py-2 text-sm font-medium text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white"
|
className="flex w-28 items-center justify-center rounded-3xl border border-solid border-purple-30 px-5 py-2 text-sm font-medium text-purple-30 transition-colors hover:bg-purple-30 hover:text-white"
|
||||||
onClick={handleGeneratePublicLink}
|
onClick={handleGeneratePublicLink}
|
||||||
>
|
>
|
||||||
{loadingStates.publicLink ? (
|
{loadingStates.publicLink ? (
|
||||||
@@ -127,11 +143,37 @@ export default function AgentDetailsModal({
|
|||||||
API Key
|
API Key
|
||||||
</h2>
|
</h2>
|
||||||
{apiKey ? (
|
{apiKey ? (
|
||||||
<span className="font-mono text-sm text-gray-700 dark:text-[#ECECF1]">
|
<div className="flex flex-col gap-2">
|
||||||
{apiKey}
|
<div className="flex items-center gap-2">
|
||||||
</span>
|
<div className="break-all font-roboto text-[14px] font-medium leading-normal text-gray-700 dark:text-[#ECECF1]">
|
||||||
|
{apiKey}
|
||||||
|
{!apiKey.includes('...') && (
|
||||||
|
<CopyButton
|
||||||
|
textToCopy={apiKey}
|
||||||
|
padding="p-1"
|
||||||
|
className="absolute -mt-0.5 ml-1 inline-flex"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!apiKey.includes('...') && (
|
||||||
|
<a
|
||||||
|
href={`https://widget.docsgpt.cloud/?api-key=${apiKey}`}
|
||||||
|
className="group ml-8 flex w-[101px] items-center justify-center gap-1 rounded-[62px] border border-purple-30 py-1.5 text-sm font-medium text-purple-30 transition-colors hover:bg-purple-30 hover:text-white"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Test
|
||||||
|
<img
|
||||||
|
src="/src/assets/external-link.svg"
|
||||||
|
alt="External link"
|
||||||
|
className="h-3 w-3 group-hover:brightness-0 group-hover:invert"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button className="hover:bg-vi</button>olets-are-blue w-28 rounded-3xl border border-solid border-violets-are-blue px-5 py-2 text-sm font-medium text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white">
|
<button className="w-28 rounded-3xl border border-solid border-purple-30 px-5 py-2 text-sm font-medium text-purple-30 transition-colors hover:bg-purple-30 hover:text-white">
|
||||||
Generate
|
Generate
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -141,21 +183,36 @@ export default function AgentDetailsModal({
|
|||||||
<h2 className="text-base font-semibold text-jet dark:text-bright-gray">
|
<h2 className="text-base font-semibold text-jet dark:text-bright-gray">
|
||||||
Webhook URL
|
Webhook URL
|
||||||
</h2>
|
</h2>
|
||||||
{webhookUrl && (
|
|
||||||
<div className="mb-1">
|
|
||||||
<CopyButton textToCopy={webhookUrl} padding="p-1" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{webhookUrl ? (
|
{webhookUrl ? (
|
||||||
<div className="flex flex-col flex-wrap items-start gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<p className="f break-all font-mono text-sm text-gray-700 dark:text-[#ECECF1]">
|
<p className="break-all font-roboto text-[14px] font-medium leading-normal text-gray-700 dark:text-[#ECECF1]">
|
||||||
{webhookUrl}
|
<a href={webhookUrl} target="_blank" rel="noreferrer">
|
||||||
|
{webhookUrl}
|
||||||
|
</a>
|
||||||
|
<CopyButton
|
||||||
|
textToCopy={webhookUrl}
|
||||||
|
padding="p-1"
|
||||||
|
className="absolute -mt-0.5 ml-1 inline-flex"
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
<a
|
||||||
|
href="https://docs.docsgpt.cloud/Agents/basics#core-components-of-an-agent"
|
||||||
|
className="flex w-fit items-center gap-1 text-purple-30 hover:underline"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<span className="text-sm">Learn more</span>
|
||||||
|
<img
|
||||||
|
src="/src/assets/external-link.svg"
|
||||||
|
alt="External link"
|
||||||
|
className="h-3 w-3"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
className="hover:bg-vi</button>olets-are-blue flex w-28 items-center justify-center rounded-3xl border border-solid border-violets-are-blue px-5 py-2 text-sm font-medium text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white"
|
className="flex w-28 items-center justify-center rounded-3xl border border-solid border-purple-30 px-5 py-2 text-sm font-medium text-purple-30 transition-colors hover:bg-purple-30 hover:text-white"
|
||||||
onClick={handleGenerateWebhook}
|
onClick={handleGenerateWebhook}
|
||||||
>
|
>
|
||||||
{loadingStates.webhook ? (
|
{loadingStates.webhook ? (
|
||||||
|
|||||||
@@ -184,29 +184,54 @@ export default function PromptsModal({
|
|||||||
setEditPromptName: (name: string) => void;
|
setEditPromptName: (name: string) => void;
|
||||||
editPromptContent: string;
|
editPromptContent: string;
|
||||||
setEditPromptContent: (content: string) => void;
|
setEditPromptContent: (content: string) => void;
|
||||||
currentPromptEdit: { name: string; id: string; type: string };
|
currentPromptEdit: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
content?: string;
|
||||||
|
};
|
||||||
handleAddPrompt?: () => void;
|
handleAddPrompt?: () => void;
|
||||||
handleEditPrompt?: (id: string, type: string) => void;
|
handleEditPrompt?: (id: string, type: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [disableSave, setDisableSave] = React.useState(true);
|
const [disableSave, setDisableSave] = React.useState(true);
|
||||||
const handlePrompNameChange = (edit: boolean, newName: string) => {
|
const handlePromptNameChange = (edit: boolean, newName: string) => {
|
||||||
const nameExists = existingPrompts.find(
|
|
||||||
(prompt) => newName === prompt.name,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newName && !nameExists) {
|
|
||||||
setDisableSave(false);
|
|
||||||
} else {
|
|
||||||
setDisableSave(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (edit) {
|
if (edit) {
|
||||||
|
const nameExists = existingPrompts.find(
|
||||||
|
(prompt) =>
|
||||||
|
newName === prompt.name && prompt.id !== currentPromptEdit.id,
|
||||||
|
);
|
||||||
|
const nameValid = newName && !nameExists;
|
||||||
|
const contentChanged = editPromptContent !== currentPromptEdit.content;
|
||||||
|
|
||||||
|
setDisableSave(!(nameValid || contentChanged));
|
||||||
setEditPromptName(newName);
|
setEditPromptName(newName);
|
||||||
} else {
|
} else {
|
||||||
|
const nameExists = existingPrompts.find(
|
||||||
|
(prompt) => newName === prompt.name,
|
||||||
|
);
|
||||||
|
setDisableSave(!(newName && !nameExists));
|
||||||
setNewPromptName(newName);
|
setNewPromptName(newName);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleContentChange = (edit: boolean, newContent: string) => {
|
||||||
|
if (edit) {
|
||||||
|
const contentChanged = newContent !== currentPromptEdit.content;
|
||||||
|
const nameValid =
|
||||||
|
editPromptName &&
|
||||||
|
!existingPrompts.find(
|
||||||
|
(prompt) =>
|
||||||
|
editPromptName === prompt.name &&
|
||||||
|
prompt.id !== currentPromptEdit.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
setDisableSave(!(nameValid || contentChanged));
|
||||||
|
setEditPromptContent(newContent);
|
||||||
|
} else {
|
||||||
|
setNewPromptContent(newContent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let view;
|
let view;
|
||||||
|
|
||||||
if (type === 'ADD') {
|
if (type === 'ADD') {
|
||||||
@@ -215,9 +240,9 @@ export default function PromptsModal({
|
|||||||
setModalState={setModalState}
|
setModalState={setModalState}
|
||||||
handleAddPrompt={handleAddPrompt}
|
handleAddPrompt={handleAddPrompt}
|
||||||
newPromptName={newPromptName}
|
newPromptName={newPromptName}
|
||||||
setNewPromptName={handlePrompNameChange.bind(null, false)}
|
setNewPromptName={handlePromptNameChange.bind(null, false)}
|
||||||
newPromptContent={newPromptContent}
|
newPromptContent={newPromptContent}
|
||||||
setNewPromptContent={setNewPromptContent}
|
setNewPromptContent={handleContentChange.bind(null, false)}
|
||||||
disableSave={disableSave}
|
disableSave={disableSave}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -227,9 +252,9 @@ export default function PromptsModal({
|
|||||||
setModalState={setModalState}
|
setModalState={setModalState}
|
||||||
handleEditPrompt={handleEditPrompt}
|
handleEditPrompt={handleEditPrompt}
|
||||||
editPromptName={editPromptName}
|
editPromptName={editPromptName}
|
||||||
setEditPromptName={handlePrompNameChange.bind(null, true)}
|
setEditPromptName={handlePromptNameChange.bind(null, true)}
|
||||||
editPromptContent={editPromptContent}
|
editPromptContent={editPromptContent}
|
||||||
setEditPromptContent={setEditPromptContent}
|
setEditPromptContent={handleContentChange.bind(null, true)}
|
||||||
currentPromptEdit={currentPromptEdit}
|
currentPromptEdit={currentPromptEdit}
|
||||||
disableSave={disableSave}
|
disableSave={disableSave}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Dropdown from '../components/Dropdown';
|
|||||||
import { ActiveState, PromptProps } from '../models/misc';
|
import { ActiveState, PromptProps } from '../models/misc';
|
||||||
import { selectToken } from '../preferences/preferenceSlice';
|
import { selectToken } from '../preferences/preferenceSlice';
|
||||||
import PromptsModal from '../preferences/PromptsModal';
|
import PromptsModal from '../preferences/PromptsModal';
|
||||||
|
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||||
|
|
||||||
export default function Prompts({
|
export default function Prompts({
|
||||||
prompts,
|
prompts,
|
||||||
@@ -40,6 +41,11 @@ export default function Prompts({
|
|||||||
const [modalState, setModalState] = React.useState<ActiveState>('INACTIVE');
|
const [modalState, setModalState] = React.useState<ActiveState>('INACTIVE');
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [promptToDelete, setPromptToDelete] = React.useState<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const handleAddPrompt = async () => {
|
const handleAddPrompt = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await userService.createPrompt(
|
const response = await userService.createPrompt(
|
||||||
@@ -69,20 +75,37 @@ export default function Prompts({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeletePrompt = (id: string) => {
|
const handleDeletePrompt = (id: string) => {
|
||||||
setPrompts(prompts.filter((prompt) => prompt.id !== id));
|
const promptToRemove = prompts.find((prompt) => prompt.id === id);
|
||||||
userService
|
if (promptToRemove) {
|
||||||
.deletePrompt({ id }, token)
|
setPromptToDelete({ id, name: promptToRemove.name });
|
||||||
.then((response) => {
|
}
|
||||||
if (!response.ok) {
|
};
|
||||||
throw new Error('Failed to delete prompt');
|
|
||||||
}
|
const confirmDeletePrompt = () => {
|
||||||
if (prompts.length > 0) {
|
if (promptToDelete) {
|
||||||
onSelectPrompt(prompts[0].name, prompts[0].id, prompts[0].type);
|
setPrompts(prompts.filter((prompt) => prompt.id !== promptToDelete.id));
|
||||||
}
|
userService
|
||||||
})
|
.deletePrompt({ id: promptToDelete.id }, token)
|
||||||
.catch((error) => {
|
.then((response) => {
|
||||||
console.error(error);
|
if (!response.ok) {
|
||||||
});
|
throw new Error('Failed to delete prompt');
|
||||||
|
}
|
||||||
|
if (prompts.length > 0) {
|
||||||
|
const firstPrompt = prompts.find((p) => p.id !== promptToDelete.id);
|
||||||
|
if (firstPrompt) {
|
||||||
|
onSelectPrompt(
|
||||||
|
firstPrompt.name,
|
||||||
|
firstPrompt.id,
|
||||||
|
firstPrompt.type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
setPromptToDelete(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFetchPromptContent = async (id: string) => {
|
const handleFetchPromptContent = async (id: string) => {
|
||||||
@@ -202,6 +225,19 @@ export default function Prompts({
|
|||||||
handleAddPrompt={handleAddPrompt}
|
handleAddPrompt={handleAddPrompt}
|
||||||
handleEditPrompt={handleSaveChanges}
|
handleEditPrompt={handleSaveChanges}
|
||||||
/>
|
/>
|
||||||
|
{promptToDelete && (
|
||||||
|
<ConfirmationModal
|
||||||
|
message={t('modals.prompts.deleteConfirmation', {
|
||||||
|
name: promptToDelete.name,
|
||||||
|
})}
|
||||||
|
modalState="ACTIVE"
|
||||||
|
setModalState={() => setPromptToDelete(null)}
|
||||||
|
submitLabel={t('modals.deleteConv.delete')}
|
||||||
|
handleSubmit={confirmDeletePrompt}
|
||||||
|
handleCancel={() => setPromptToDelete(null)}
|
||||||
|
variant="danger"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
prefListenerMiddleware,
|
prefListenerMiddleware,
|
||||||
prefSlice,
|
prefSlice,
|
||||||
} from './preferences/preferenceSlice';
|
} from './preferences/preferenceSlice';
|
||||||
|
import uploadReducer from './upload/uploadSlice';
|
||||||
|
|
||||||
const key = localStorage.getItem('DocsGPTApiKey');
|
const key = localStorage.getItem('DocsGPTApiKey');
|
||||||
const prompt = localStorage.getItem('DocsGPTPrompt');
|
const prompt = localStorage.getItem('DocsGPTPrompt');
|
||||||
@@ -52,6 +53,7 @@ const store = configureStore({
|
|||||||
preference: prefSlice.reducer,
|
preference: prefSlice.reducer,
|
||||||
conversation: conversationSlice.reducer,
|
conversation: conversationSlice.reducer,
|
||||||
sharedConversation: sharedConversationSlice.reducer,
|
sharedConversation: sharedConversationSlice.reducer,
|
||||||
|
upload: uploadReducer,
|
||||||
},
|
},
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) =>
|
||||||
getDefaultMiddleware().concat(prefListenerMiddleware.middleware),
|
getDefaultMiddleware().concat(prefListenerMiddleware.middleware),
|
||||||
|
|||||||
69
frontend/src/upload/uploadSlice.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { RootState } from '../store';
|
||||||
|
|
||||||
|
export interface Attachment {
|
||||||
|
fileName: string;
|
||||||
|
progress: number;
|
||||||
|
status: 'uploading' | 'processing' | 'completed' | 'failed';
|
||||||
|
taskId: string;
|
||||||
|
id?: string;
|
||||||
|
token_count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UploadState {
|
||||||
|
attachments: Attachment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: UploadState = {
|
||||||
|
attachments: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadSlice = createSlice({
|
||||||
|
name: 'upload',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
clearAttachments: (state) => {
|
||||||
|
state.attachments = state.attachments.filter(
|
||||||
|
(att) => att.status === 'uploading' || att.status === 'processing',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
addAttachment,
|
||||||
|
updateAttachment,
|
||||||
|
removeAttachment,
|
||||||
|
clearAttachments,
|
||||||
|
} = uploadSlice.actions;
|
||||||
|
|
||||||
|
export const selectAttachments = (state: RootState) => state.upload.attachments;
|
||||||
|
export const selectCompletedAttachments = (state: RootState) =>
|
||||||
|
state.upload.attachments.filter((att) => att.status === 'completed');
|
||||||
|
|
||||||
|
export default uploadSlice.reducer;
|
||||||
@@ -4,6 +4,9 @@ module.exports = {
|
|||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
'roboto': ['Roboto', 'sans-serif'],
|
||||||
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
112: '28rem',
|
112: '28rem',
|
||||||
128: '32rem',
|
128: '32rem',
|
||||||
|
|||||||
31
setup.sh
@@ -169,7 +169,7 @@ prompt_ollama_options() {
|
|||||||
# 1) Use DocsGPT Public API Endpoint (simple and free)
|
# 1) Use DocsGPT Public API Endpoint (simple and free)
|
||||||
use_docs_public_api_endpoint() {
|
use_docs_public_api_endpoint() {
|
||||||
echo -e "\n${NC}Setting up DocsGPT Public API Endpoint...${NC}"
|
echo -e "\n${NC}Setting up DocsGPT Public API Endpoint...${NC}"
|
||||||
echo "LLM_NAME=docsgpt" > .env
|
echo "LLM_PROVIDER=docsgpt" > .env
|
||||||
echo "VITE_API_STREAMING=true" >> .env
|
echo "VITE_API_STREAMING=true" >> .env
|
||||||
echo -e "${GREEN}.env file configured for DocsGPT Public API.${NC}"
|
echo -e "${GREEN}.env file configured for DocsGPT Public API.${NC}"
|
||||||
|
|
||||||
@@ -237,13 +237,12 @@ serve_local_ollama() {
|
|||||||
|
|
||||||
echo -e "\n${NC}Configuring for Ollama ($(echo "$docker_compose_file_suffix" | tr '[:lower:]' '[:upper:]'))...${NC}" # Using tr for uppercase - more compatible
|
echo -e "\n${NC}Configuring for Ollama ($(echo "$docker_compose_file_suffix" | tr '[:lower:]' '[:upper:]'))...${NC}" # Using tr for uppercase - more compatible
|
||||||
echo "API_KEY=xxxx" > .env # Placeholder API Key
|
echo "API_KEY=xxxx" > .env # Placeholder API Key
|
||||||
echo "LLM_NAME=openai" >> .env
|
echo "LLM_PROVIDER=openai" >> .env
|
||||||
echo "MODEL_NAME=$model_name" >> .env
|
echo "LLM_NAME=$model_name" >> .env
|
||||||
echo "VITE_API_STREAMING=true" >> .env
|
echo "VITE_API_STREAMING=true" >> .env
|
||||||
echo "OPENAI_BASE_URL=http://ollama:11434/v1" >> .env
|
echo "OPENAI_BASE_URL=http://ollama:11434/v1" >> .env
|
||||||
echo "EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2" >> .env
|
echo "EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2" >> .env
|
||||||
echo -e "${GREEN}.env file configured for Ollama ($(echo "$docker_compose_file_suffix" | tr '[:lower:]' '[:upper:]')${NC}${GREEN}).${NC}"
|
echo -e "${GREEN}.env file configured for Ollama ($(echo "$docker_compose_file_suffix" | tr '[:lower:]' '[:upper:]')${NC}${GREEN}).${NC}"
|
||||||
echo -e "${YELLOW}Note: MODEL_NAME is set to '${BOLD}$model_name${NC}${YELLOW}'. You can change it later in the .env file.${NC}"
|
|
||||||
|
|
||||||
|
|
||||||
check_and_start_docker
|
check_and_start_docker
|
||||||
@@ -350,8 +349,8 @@ connect_local_inference_engine() {
|
|||||||
|
|
||||||
echo -e "\n${NC}Configuring for Local Inference Engine: ${BOLD}${engine_name}...${NC}"
|
echo -e "\n${NC}Configuring for Local Inference Engine: ${BOLD}${engine_name}...${NC}"
|
||||||
echo "API_KEY=None" > .env
|
echo "API_KEY=None" > .env
|
||||||
echo "LLM_NAME=openai" >> .env
|
echo "LLM_PROVIDER=openai" >> .env
|
||||||
echo "MODEL_NAME=$model_name" >> .env
|
echo "LLM_NAME=$model_name" >> .env
|
||||||
echo "VITE_API_STREAMING=true" >> .env
|
echo "VITE_API_STREAMING=true" >> .env
|
||||||
echo "OPENAI_BASE_URL=$openai_base_url" >> .env
|
echo "OPENAI_BASE_URL=$openai_base_url" >> .env
|
||||||
echo "EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2" >> .env
|
echo "EMBEDDINGS_NAME=huggingface_sentence-transformers/all-mpnet-base-v2" >> .env
|
||||||
@@ -381,7 +380,7 @@ connect_local_inference_engine() {
|
|||||||
|
|
||||||
# 4) Connect Cloud API Provider
|
# 4) Connect Cloud API Provider
|
||||||
connect_cloud_api_provider() {
|
connect_cloud_api_provider() {
|
||||||
local provider_choice api_key llm_name
|
local provider_choice api_key llm_provider
|
||||||
local setup_result # Variable to store the return status
|
local setup_result # Variable to store the return status
|
||||||
|
|
||||||
get_api_key() {
|
get_api_key() {
|
||||||
@@ -395,43 +394,43 @@ connect_cloud_api_provider() {
|
|||||||
case "$provider_choice" in
|
case "$provider_choice" in
|
||||||
1) # OpenAI
|
1) # OpenAI
|
||||||
provider_name="OpenAI"
|
provider_name="OpenAI"
|
||||||
llm_name="openai"
|
llm_provider="openai"
|
||||||
model_name="gpt-4o"
|
model_name="gpt-4o"
|
||||||
get_api_key
|
get_api_key
|
||||||
break ;;
|
break ;;
|
||||||
2) # Google
|
2) # Google
|
||||||
provider_name="Google (Vertex AI, Gemini)"
|
provider_name="Google (Vertex AI, Gemini)"
|
||||||
llm_name="google"
|
llm_provider="google"
|
||||||
model_name="gemini-2.0-flash"
|
model_name="gemini-2.0-flash"
|
||||||
get_api_key
|
get_api_key
|
||||||
break ;;
|
break ;;
|
||||||
3) # Anthropic
|
3) # Anthropic
|
||||||
provider_name="Anthropic (Claude)"
|
provider_name="Anthropic (Claude)"
|
||||||
llm_name="anthropic"
|
llm_provider="anthropic"
|
||||||
model_name="claude-3-5-sonnet-latest"
|
model_name="claude-3-5-sonnet-latest"
|
||||||
get_api_key
|
get_api_key
|
||||||
break ;;
|
break ;;
|
||||||
4) # Groq
|
4) # Groq
|
||||||
provider_name="Groq"
|
provider_name="Groq"
|
||||||
llm_name="groq"
|
llm_provider="groq"
|
||||||
model_name="llama-3.1-8b-instant"
|
model_name="llama-3.1-8b-instant"
|
||||||
get_api_key
|
get_api_key
|
||||||
break ;;
|
break ;;
|
||||||
5) # HuggingFace Inference API
|
5) # HuggingFace Inference API
|
||||||
provider_name="HuggingFace Inference API"
|
provider_name="HuggingFace Inference API"
|
||||||
llm_name="huggingface"
|
llm_provider="huggingface"
|
||||||
model_name="meta-llama/Llama-3.1-8B-Instruct"
|
model_name="meta-llama/Llama-3.1-8B-Instruct"
|
||||||
get_api_key
|
get_api_key
|
||||||
break ;;
|
break ;;
|
||||||
6) # Azure OpenAI
|
6) # Azure OpenAI
|
||||||
provider_name="Azure OpenAI"
|
provider_name="Azure OpenAI"
|
||||||
llm_name="azure_openai"
|
llm_provider="azure_openai"
|
||||||
model_name="gpt-4o"
|
model_name="gpt-4o"
|
||||||
get_api_key
|
get_api_key
|
||||||
break ;;
|
break ;;
|
||||||
7) # Novita
|
7) # Novita
|
||||||
provider_name="Novita"
|
provider_name="Novita"
|
||||||
llm_name="novita"
|
llm_provider="novita"
|
||||||
model_name="deepseek/deepseek-r1"
|
model_name="deepseek/deepseek-r1"
|
||||||
get_api_key
|
get_api_key
|
||||||
break ;;
|
break ;;
|
||||||
@@ -442,8 +441,8 @@ connect_cloud_api_provider() {
|
|||||||
|
|
||||||
echo -e "\n${NC}Configuring for Cloud API Provider: ${BOLD}${provider_name}...${NC}"
|
echo -e "\n${NC}Configuring for Cloud API Provider: ${BOLD}${provider_name}...${NC}"
|
||||||
echo "API_KEY=$api_key" > .env
|
echo "API_KEY=$api_key" > .env
|
||||||
echo "LLM_NAME=$llm_name" >> .env
|
echo "LLM_PROVIDER=$llm_provider" >> .env
|
||||||
echo "MODEL_NAME=$model_name" >> .env
|
echo "LLM_NAME=$model_name" >> .env
|
||||||
echo "VITE_API_STREAMING=true" >> .env
|
echo "VITE_API_STREAMING=true" >> .env
|
||||||
echo -e "${GREEN}.env file configured for ${BOLD}${provider_name}${NC}${GREEN}.${NC}"
|
echo -e "${GREEN}.env file configured for ${BOLD}${provider_name}${NC}${GREEN}.${NC}"
|
||||||
|
|
||||||
|
|||||||