mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
Compare commits
50 Commits
dependabot
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4675f4cccc | ||
|
|
4ff99a1e86 | ||
|
|
129084ba92 | ||
|
|
2288df1293 | ||
|
|
d9dfac55e7 | ||
|
|
404cf4b7c7 | ||
|
|
f1c1fc123b | ||
|
|
9f19c7ee4c | ||
|
|
155e74eca1 | ||
|
|
ea2dc4dbcb | ||
|
|
616edc97de | ||
|
|
b017e99c79 | ||
|
|
f698e9d3e1 | ||
|
|
d366502850 | ||
|
|
3d6757c170 | ||
|
|
cb8302add8 | ||
|
|
9d266e9fad | ||
|
|
ae94c9d31e | ||
|
|
83ab232dcd | ||
|
|
eea85772a3 | ||
|
|
0fe7e223cc | ||
|
|
3789d2eb03 | ||
|
|
d54469532e | ||
|
|
9884e51836 | ||
|
|
6626723180 | ||
|
|
0c251e066b | ||
|
|
0957034bfa | ||
|
|
44521cd893 | ||
|
|
b17f846730 | ||
|
|
6dd32fd4ca | ||
|
|
b17b1c70b5 | ||
|
|
3f5b31fb5f | ||
|
|
06bda6bd55 | ||
|
|
7dd97821a8 | ||
|
|
695191d888 | ||
|
|
1dbcef24c7 | ||
|
|
e086c79da0 | ||
|
|
6ae8d34b27 | ||
|
|
2e23e547d3 | ||
|
|
fa11dc9828 | ||
|
|
673fa70bc5 | ||
|
|
a0660a54c1 | ||
|
|
1137bf4280 | ||
|
|
da41c898d8 | ||
|
|
21e5c261ef | ||
|
|
a7d61b9d59 | ||
|
|
c5fe25c149 | ||
|
|
6a4cb617f9 | ||
|
|
94f70e6de5 | ||
|
|
ab4ebf9a9d |
11
.github/styles/DocsGPT/Spelling.yml
vendored
Normal file
11
.github/styles/DocsGPT/Spelling.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
extends: spelling
|
||||
level: warning
|
||||
message: "Did you really mean '%s'?"
|
||||
ignore:
|
||||
- "**/node_modules/**"
|
||||
- "**/dist/**"
|
||||
- "**/build/**"
|
||||
- "**/coverage/**"
|
||||
- "**/public/**"
|
||||
- "**/static/**"
|
||||
vocab: DocsGPT
|
||||
46
.github/styles/config/vocabularies/DocsGPT/accept.txt
vendored
Normal file
46
.github/styles/config/vocabularies/DocsGPT/accept.txt
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
Ollama
|
||||
Qdrant
|
||||
Milvus
|
||||
Chatwoot
|
||||
Nextra
|
||||
VSCode
|
||||
npm
|
||||
LLMs
|
||||
APIs
|
||||
Groq
|
||||
SGLang
|
||||
LMDeploy
|
||||
OAuth
|
||||
Vite
|
||||
LLM
|
||||
JSONPath
|
||||
UIs
|
||||
configs
|
||||
uncomment
|
||||
qdrant
|
||||
vectorstore
|
||||
docsgpt
|
||||
llm
|
||||
GPUs
|
||||
kubectl
|
||||
Lightsail
|
||||
enqueues
|
||||
chatbot
|
||||
VSCode's
|
||||
Shareability
|
||||
feedbacks
|
||||
automations
|
||||
Premade
|
||||
Signup
|
||||
Repo
|
||||
repo
|
||||
env
|
||||
URl
|
||||
agentic
|
||||
llama_cpp
|
||||
parsable
|
||||
SDKs
|
||||
boolean
|
||||
bool
|
||||
hardcode
|
||||
EOL
|
||||
26
.github/workflows/vale.yml
vendored
Normal file
26
.github/workflows/vale.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Vale Documentation Linter
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'docs/**/*.md'
|
||||
- 'docs/**/*.mdx'
|
||||
- '**/*.md'
|
||||
- '.vale.ini'
|
||||
- '.github/styles/**'
|
||||
|
||||
jobs:
|
||||
vale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Vale linter
|
||||
uses: errata-ai/vale-action@v2
|
||||
with:
|
||||
files: docs
|
||||
fail_on_error: false
|
||||
version: 3.0.5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@ __pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
experiments
|
||||
# C extensions
|
||||
*.so
|
||||
*.next
|
||||
|
||||
5
.vale.ini
Normal file
5
.vale.ini
Normal file
@@ -0,0 +1,5 @@
|
||||
MinAlertLevel = warning
|
||||
StylesPath = .github/styles
|
||||
|
||||
[*.{md,mdx}]
|
||||
BasedOnStyles = DocsGPT
|
||||
@@ -147,5 +147,5 @@ Here's a step-by-step guide on how to contribute to DocsGPT:
|
||||
Thank you for considering contributing to DocsGPT! 🙏
|
||||
|
||||
## Questions/collaboration
|
||||
Feel free to join our [Discord](https://discord.gg/n5BX8dh8rU). We're very friendly and welcoming to new contributors, so don't hesitate to reach out.
|
||||
Feel free to join our [Discord](https://discord.gg/vN7YFfdMpj). We're very friendly and welcoming to new contributors, so don't hesitate to reach out.
|
||||
# Thank you so much for considering to contributing DocsGPT!🙏
|
||||
|
||||
@@ -32,7 +32,7 @@ Non-Code Contributions:
|
||||
- Before contributing check existing [issues](https://github.com/arc53/DocsGPT/issues) or [create](https://github.com/arc53/DocsGPT/issues/new/choose) an issue and wait to get assigned.
|
||||
- Once you are finished with your contribution, please fill in this [form](https://forms.gle/Npaba4n9Epfyx56S8).
|
||||
- Refer to the [Documentation](https://docs.docsgpt.cloud/).
|
||||
- Feel free to join our [Discord](https://discord.gg/n5BX8dh8rU) server. We're here to help newcomers, so don't hesitate to jump in! Join us [here](https://discord.gg/n5BX8dh8rU).
|
||||
- Feel free to join our [Discord](https://discord.gg/vN7YFfdMpj) server. We're here to help newcomers, so don't hesitate to jump in! Join us [here](https://discord.gg/vN7YFfdMpj).
|
||||
|
||||
Thank you very much for considering contributing to DocsGPT during Hacktoberfest! 🙏 Your contributions (not just simple typos) could earn you a stylish new t-shirt.
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
<a href="https://github.com/arc53/DocsGPT"></a>
|
||||
<a href="https://github.com/arc53/DocsGPT/blob/main/LICENSE"></a>
|
||||
<a href="https://www.bestpractices.dev/projects/9907"><img src="https://www.bestpractices.dev/projects/9907/badge"></a>
|
||||
<a href="https://discord.gg/n5BX8dh8rU"></a>
|
||||
<a href="https://discord.gg/vN7YFfdMpj"></a>
|
||||
<a href="https://x.com/docsgptai"></a>
|
||||
|
||||
<a href="https://docs.docsgpt.cloud/quickstart">⚡️ Quickstart</a> • <a href="https://app.docsgpt.cloud/">☁️ Cloud Version</a> • <a href="https://discord.gg/n5BX8dh8rU">💬 Discord</a>
|
||||
<a href="https://docs.docsgpt.cloud/quickstart">⚡️ Quickstart</a> • <a href="https://app.docsgpt.cloud/">☁️ Cloud Version</a> • <a href="https://discord.gg/vN7YFfdMpj">💬 Discord</a>
|
||||
<br>
|
||||
<a href="https://docs.docsgpt.cloud/">📖 Documentation</a> • <a href="https://github.com/arc53/DocsGPT/blob/main/CONTRIBUTING.md">👫 Contribute</a> • <a href="https://blog.docsgpt.cloud/">🗞 Blog</a>
|
||||
<br>
|
||||
|
||||
@@ -12,7 +12,6 @@ from application.core.settings import settings
|
||||
from application.llm.handlers.handler_creator import LLMHandlerCreator
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from application.logging import build_stack_data, log_activity, LogContext
|
||||
from application.retriever.base import BaseRetriever
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,6 +26,7 @@ class BaseAgent(ABC):
|
||||
user_api_key: Optional[str] = None,
|
||||
prompt: str = "",
|
||||
chat_history: Optional[List[Dict]] = None,
|
||||
retrieved_docs: Optional[List[Dict]] = None,
|
||||
decoded_token: Optional[Dict] = None,
|
||||
attachments: Optional[List[Dict]] = None,
|
||||
json_schema: Optional[Dict] = None,
|
||||
@@ -53,6 +53,7 @@ class BaseAgent(ABC):
|
||||
user_api_key=user_api_key,
|
||||
decoded_token=decoded_token,
|
||||
)
|
||||
self.retrieved_docs = retrieved_docs or []
|
||||
self.llm_handler = LLMHandlerCreator.create_handler(
|
||||
llm_name if llm_name else "default"
|
||||
)
|
||||
@@ -65,13 +66,13 @@ class BaseAgent(ABC):
|
||||
|
||||
@log_activity()
|
||||
def gen(
|
||||
self, query: str, retriever: BaseRetriever, log_context: LogContext = None
|
||||
self, query: str, log_context: LogContext = None
|
||||
) -> Generator[Dict, None, None]:
|
||||
yield from self._gen_inner(query, retriever, log_context)
|
||||
yield from self._gen_inner(query, log_context)
|
||||
|
||||
@abstractmethod
|
||||
def _gen_inner(
|
||||
self, query: str, retriever: BaseRetriever, log_context: LogContext
|
||||
self, query: str, log_context: LogContext
|
||||
) -> Generator[Dict, None, None]:
|
||||
pass
|
||||
|
||||
@@ -150,6 +151,7 @@ class BaseAgent(ABC):
|
||||
call_id = getattr(call, "id", None) or str(uuid.uuid4())
|
||||
|
||||
# Check if parsing failed
|
||||
|
||||
if tool_id is None or action_name is None:
|
||||
error_message = f"Error: Failed to parse LLM tool call. Tool name: {getattr(call, 'name', 'unknown')}"
|
||||
logger.error(error_message)
|
||||
@@ -164,13 +166,14 @@ class BaseAgent(ABC):
|
||||
yield {"type": "tool_call", "data": {**tool_call_data, "status": "error"}}
|
||||
self.tool_calls.append(tool_call_data)
|
||||
return "Failed to parse tool call.", call_id
|
||||
|
||||
# Check if tool_id exists in available tools
|
||||
|
||||
if tool_id not in tools_dict:
|
||||
error_message = f"Error: Tool ID '{tool_id}' extracted from LLM call not found in available tools_dict. Available IDs: {list(tools_dict.keys())}"
|
||||
logger.error(error_message)
|
||||
|
||||
# Return error result
|
||||
|
||||
tool_call_data = {
|
||||
"tool_name": "unknown",
|
||||
"call_id": call_id,
|
||||
@@ -181,7 +184,6 @@ class BaseAgent(ABC):
|
||||
yield {"type": "tool_call", "data": {**tool_call_data, "status": "error"}}
|
||||
self.tool_calls.append(tool_call_data)
|
||||
return f"Tool with ID {tool_id} not found.", call_id
|
||||
|
||||
tool_call_data = {
|
||||
"tool_name": tools_dict[tool_id]["name"],
|
||||
"call_id": call_id,
|
||||
@@ -223,6 +225,7 @@ class BaseAgent(ABC):
|
||||
tm = ToolManager(config={})
|
||||
|
||||
# Prepare tool_config and add tool_id for memory tools
|
||||
|
||||
if tool_data["name"] == "api_tool":
|
||||
tool_config = {
|
||||
"url": tool_data["config"]["actions"][action_name]["url"],
|
||||
@@ -234,8 +237,8 @@ class BaseAgent(ABC):
|
||||
tool_config = tool_data["config"].copy() if tool_data["config"] else {}
|
||||
# Add tool_id from MongoDB _id for tools that need instance isolation (like memory tool)
|
||||
# Use MongoDB _id if available, otherwise fall back to enumerated tool_id
|
||||
tool_config["tool_id"] = str(tool_data.get("_id", tool_id))
|
||||
|
||||
tool_config["tool_id"] = str(tool_data.get("_id", tool_id))
|
||||
tool = tm.load_tool(
|
||||
tool_data["name"],
|
||||
tool_config=tool_config,
|
||||
@@ -276,24 +279,14 @@ class BaseAgent(ABC):
|
||||
self,
|
||||
system_prompt: str,
|
||||
query: str,
|
||||
retrieved_data: List[Dict],
|
||||
) -> List[Dict]:
|
||||
docs_with_filenames = []
|
||||
for doc in retrieved_data:
|
||||
filename = doc.get("filename") or doc.get("title") or doc.get("source")
|
||||
if filename:
|
||||
chunk_header = str(filename)
|
||||
docs_with_filenames.append(f"{chunk_header}\n{doc['text']}")
|
||||
else:
|
||||
docs_with_filenames.append(doc["text"])
|
||||
docs_together = "\n\n".join(docs_with_filenames)
|
||||
p_chat_combine = system_prompt.replace("{summaries}", docs_together)
|
||||
messages_combine = [{"role": "system", "content": p_chat_combine}]
|
||||
"""Build messages using pre-rendered system prompt"""
|
||||
messages = [{"role": "system", "content": system_prompt}]
|
||||
|
||||
for i in self.chat_history:
|
||||
if "prompt" in i and "response" in i:
|
||||
messages_combine.append({"role": "user", "content": i["prompt"]})
|
||||
messages_combine.append({"role": "assistant", "content": i["response"]})
|
||||
messages.append({"role": "user", "content": i["prompt"]})
|
||||
messages.append({"role": "assistant", "content": i["response"]})
|
||||
if "tool_calls" in i:
|
||||
for tool_call in i["tool_calls"]:
|
||||
call_id = tool_call.get("call_id") or str(uuid.uuid4())
|
||||
@@ -313,26 +306,14 @@ class BaseAgent(ABC):
|
||||
}
|
||||
}
|
||||
|
||||
messages_combine.append(
|
||||
messages.append(
|
||||
{"role": "assistant", "content": [function_call_dict]}
|
||||
)
|
||||
messages_combine.append(
|
||||
messages.append(
|
||||
{"role": "tool", "content": [function_response_dict]}
|
||||
)
|
||||
messages_combine.append({"role": "user", "content": query})
|
||||
return messages_combine
|
||||
|
||||
def _retriever_search(
|
||||
self,
|
||||
retriever: BaseRetriever,
|
||||
query: str,
|
||||
log_context: Optional[LogContext] = None,
|
||||
) -> List[Dict]:
|
||||
retrieved_data = retriever.search(query)
|
||||
if log_context:
|
||||
data = build_stack_data(retriever, exclude_attributes=["llm"])
|
||||
log_context.stacks.append({"component": "retriever", "data": data})
|
||||
return retrieved_data
|
||||
messages.append({"role": "user", "content": query})
|
||||
return messages
|
||||
|
||||
def _llm_gen(self, messages: List[Dict], log_context: Optional[LogContext] = None):
|
||||
gen_kwargs = {"model": self.gpt_model, "messages": messages}
|
||||
@@ -343,7 +324,6 @@ class BaseAgent(ABC):
|
||||
and self.tools
|
||||
):
|
||||
gen_kwargs["tools"] = self.tools
|
||||
|
||||
if (
|
||||
self.json_schema
|
||||
and hasattr(self.llm, "_supports_structured_output")
|
||||
@@ -357,7 +337,6 @@ class BaseAgent(ABC):
|
||||
gen_kwargs["response_format"] = structured_format
|
||||
elif self.llm_name == "google":
|
||||
gen_kwargs["response_schema"] = structured_format
|
||||
|
||||
resp = self.llm.gen_stream(**gen_kwargs)
|
||||
|
||||
if log_context:
|
||||
|
||||
@@ -1,32 +1,20 @@
|
||||
import logging
|
||||
from typing import Dict, Generator
|
||||
|
||||
from application.agents.base import BaseAgent
|
||||
from application.logging import LogContext
|
||||
from application.retriever.base import BaseRetriever
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ClassicAgent(BaseAgent):
|
||||
"""A simplified 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.
|
||||
"""
|
||||
"""A simplified agent with clear execution flow"""
|
||||
|
||||
def _gen_inner(
|
||||
self, query: str, retriever: BaseRetriever, log_context: LogContext
|
||||
self, query: str, log_context: LogContext
|
||||
) -> Generator[Dict, None, None]:
|
||||
# Step 1: Retrieve relevant data
|
||||
retrieved_data = self._retriever_search(retriever, query, log_context)
|
||||
"""Core generator function for ClassicAgent execution flow"""
|
||||
|
||||
# Step 2: Prepare tools
|
||||
tools_dict = (
|
||||
self._get_user_tools(self.user)
|
||||
if not self.user_api_key
|
||||
@@ -34,20 +22,16 @@ class ClassicAgent(BaseAgent):
|
||||
)
|
||||
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)
|
||||
llm_response = self._llm_gen(messages, log_context)
|
||||
|
||||
# Step 4: Handle the response
|
||||
yield from self._handle_response(
|
||||
llm_response, tools_dict, messages, log_context
|
||||
)
|
||||
|
||||
# Step 5: Return metadata
|
||||
yield {"sources": retrieved_data}
|
||||
yield {"sources": self.retrieved_docs}
|
||||
yield {"tool_calls": self._get_truncated_tool_calls()}
|
||||
|
||||
# Log tool calls for debugging
|
||||
log_context.stacks.append(
|
||||
{"component": "agent", "data": {"tool_calls": self.tool_calls.copy()}}
|
||||
)
|
||||
|
||||
@@ -1,284 +1,238 @@
|
||||
import os
|
||||
from typing import Dict, Generator, List, Any
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, Generator, List
|
||||
|
||||
from application.agents.base import BaseAgent
|
||||
from application.logging import build_stack_data, LogContext
|
||||
from application.retriever.base import BaseRetriever
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MAX_ITERATIONS_REASONING = 10
|
||||
|
||||
current_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
with open(
|
||||
os.path.join(current_dir, "application/prompts", "react_planning_prompt.txt"), "r"
|
||||
) as f:
|
||||
planning_prompt_template = f.read()
|
||||
PLANNING_PROMPT_TEMPLATE = f.read()
|
||||
with open(
|
||||
os.path.join(current_dir, "application/prompts", "react_final_prompt.txt"),
|
||||
"r",
|
||||
os.path.join(current_dir, "application/prompts", "react_final_prompt.txt"), "r"
|
||||
) as f:
|
||||
final_prompt_template = f.read()
|
||||
|
||||
MAX_ITERATIONS_REASONING = 10
|
||||
FINAL_PROMPT_TEMPLATE = f.read()
|
||||
|
||||
|
||||
class ReActAgent(BaseAgent):
|
||||
"""
|
||||
Research and Action (ReAct) Agent - Advanced reasoning agent with iterative planning.
|
||||
|
||||
Implements a think-act-observe loop for complex problem-solving:
|
||||
1. Creates a strategic plan based on the query
|
||||
2. Executes tools and gathers observations
|
||||
3. Iteratively refines approach until satisfied
|
||||
4. Synthesizes final answer from all observations
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.plan: str = ""
|
||||
self.observations: List[str] = []
|
||||
|
||||
def _extract_content_from_llm_response(self, resp: Any) -> str:
|
||||
"""
|
||||
Helper to extract string content from various LLM response types.
|
||||
Handles strings, message objects (OpenAI-like), and streams.
|
||||
Adapt stream handling for your specific LLM client if not OpenAI.
|
||||
"""
|
||||
collected_content = []
|
||||
if isinstance(resp, str):
|
||||
collected_content.append(resp)
|
||||
elif ( # OpenAI non-streaming or Anthropic non-streaming (older SDK style)
|
||||
hasattr(resp, "message")
|
||||
and hasattr(resp.message, "content")
|
||||
and resp.message.content is not None
|
||||
):
|
||||
collected_content.append(resp.message.content)
|
||||
elif ( # OpenAI non-streaming (Pydantic model), Anthropic new SDK non-streaming
|
||||
hasattr(resp, "choices")
|
||||
and resp.choices
|
||||
and hasattr(resp.choices[0], "message")
|
||||
and hasattr(resp.choices[0].message, "content")
|
||||
and resp.choices[0].message.content is not None
|
||||
):
|
||||
collected_content.append(resp.choices[0].message.content) # OpenAI
|
||||
elif ( # Anthropic new SDK non-streaming content block
|
||||
hasattr(resp, "content")
|
||||
and isinstance(resp.content, list)
|
||||
and resp.content
|
||||
and hasattr(resp.content[0], "text")
|
||||
):
|
||||
collected_content.append(resp.content[0].text) # Anthropic
|
||||
else:
|
||||
# Assume resp is a stream if not a recognized object
|
||||
chunk = None
|
||||
try:
|
||||
for (
|
||||
chunk
|
||||
) in (
|
||||
resp
|
||||
): # This will fail if resp is not iterable (e.g. a non-streaming response object)
|
||||
content_piece = ""
|
||||
# OpenAI-like stream
|
||||
if (
|
||||
hasattr(chunk, "choices")
|
||||
and len(chunk.choices) > 0
|
||||
and hasattr(chunk.choices[0], "delta")
|
||||
and hasattr(chunk.choices[0].delta, "content")
|
||||
and chunk.choices[0].delta.content is not None
|
||||
):
|
||||
content_piece = chunk.choices[0].delta.content
|
||||
# Anthropic-like stream (ContentBlockDelta)
|
||||
elif (
|
||||
hasattr(chunk, "type")
|
||||
and chunk.type == "content_block_delta"
|
||||
and hasattr(chunk, "delta")
|
||||
and hasattr(chunk.delta, "text")
|
||||
):
|
||||
content_piece = chunk.delta.text
|
||||
elif isinstance(chunk, str): # Simplest case: stream of strings
|
||||
content_piece = chunk
|
||||
|
||||
if content_piece:
|
||||
collected_content.append(content_piece)
|
||||
except (
|
||||
TypeError
|
||||
): # If resp is not iterable (e.g. a final response object that wasn't caught above)
|
||||
logger.debug(
|
||||
f"Response type {type(resp)} could not be iterated as a stream. It might be a non-streaming object not handled by specific checks."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error processing potential stream chunk: {e}, chunk was: {getattr(chunk, '__dict__', chunk) if chunk is not None else 'N/A'}"
|
||||
)
|
||||
|
||||
return "".join(collected_content)
|
||||
|
||||
def _gen_inner(
|
||||
self, query: str, retriever: BaseRetriever, log_context: LogContext
|
||||
self, query: str, log_context: LogContext
|
||||
) -> Generator[Dict, None, None]:
|
||||
# Reset state for this generation call
|
||||
self.plan = ""
|
||||
self.observations = []
|
||||
retrieved_data = self._retriever_search(retriever, query, log_context)
|
||||
"""Execute ReAct reasoning loop with planning, action, and observation cycles"""
|
||||
|
||||
if self.user_api_key:
|
||||
tools_dict = self._get_tools(self.user_api_key)
|
||||
else:
|
||||
tools_dict = self._get_user_tools(self.user)
|
||||
self._reset_state()
|
||||
|
||||
tools_dict = (
|
||||
self._get_tools(self.user_api_key)
|
||||
if self.user_api_key
|
||||
else self._get_user_tools(self.user)
|
||||
)
|
||||
self._prepare_tools(tools_dict)
|
||||
|
||||
docs_together = "\n".join([doc["text"] for doc in retrieved_data])
|
||||
iterating_reasoning = 0
|
||||
while iterating_reasoning < MAX_ITERATIONS_REASONING:
|
||||
iterating_reasoning += 1
|
||||
# 1. Create Plan
|
||||
logger.info("ReActAgent: Creating plan...")
|
||||
plan_stream = self._create_plan(query, docs_together, log_context)
|
||||
current_plan_parts = []
|
||||
yield {"thought": f"Reasoning... (iteration {iterating_reasoning})\n\n"}
|
||||
for line_chunk in plan_stream:
|
||||
current_plan_parts.append(line_chunk)
|
||||
yield {"thought": line_chunk}
|
||||
self.plan = "".join(current_plan_parts)
|
||||
if self.plan:
|
||||
self.observations.append(
|
||||
f"Plan: {self.plan} Iteration: {iterating_reasoning}"
|
||||
)
|
||||
for iteration in range(1, MAX_ITERATIONS_REASONING + 1):
|
||||
yield {"thought": f"Reasoning... (iteration {iteration})\n\n"}
|
||||
|
||||
max_obs_len = 20000
|
||||
obs_str = "\n".join(self.observations)
|
||||
if len(obs_str) > max_obs_len:
|
||||
obs_str = obs_str[:max_obs_len] + "\n...[observations truncated]"
|
||||
execution_prompt_str = (
|
||||
(self.prompt or "")
|
||||
+ f"\n\nFollow this plan:\n{self.plan}"
|
||||
+ f"\n\nObservations:\n{obs_str}"
|
||||
+ f"\n\nIf there is enough data to complete user query '{query}', Respond with 'SATISFIED' only. Otherwise, continue. Dont Menstion 'SATISFIED' in your response if you are not ready. "
|
||||
)
|
||||
yield from self._planning_phase(query, log_context)
|
||||
|
||||
messages = self._build_messages(execution_prompt_str, query, retrieved_data)
|
||||
|
||||
resp_from_llm_gen = self._llm_gen(messages, log_context)
|
||||
|
||||
initial_llm_thought_content = self._extract_content_from_llm_response(
|
||||
resp_from_llm_gen
|
||||
)
|
||||
if initial_llm_thought_content:
|
||||
self.observations.append(
|
||||
f"Initial thought/response: {initial_llm_thought_content}"
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"ReActAgent: Initial LLM response (before handler) had no textual content (might be only tool calls)."
|
||||
)
|
||||
resp_after_handler = self._llm_handler(
|
||||
resp_from_llm_gen, tools_dict, messages, log_context
|
||||
)
|
||||
|
||||
for (
|
||||
tool_call_info
|
||||
) in (
|
||||
self.tool_calls
|
||||
): # Iterate over self.tool_calls populated by _llm_handler
|
||||
observation_string = (
|
||||
f"Executed Action: Tool '{tool_call_info.get('tool_name', 'N/A')}' "
|
||||
f"with arguments '{tool_call_info.get('arguments', '{}')}'. Result: '{str(tool_call_info.get('result', ''))[:200]}...'"
|
||||
)
|
||||
self.observations.append(observation_string)
|
||||
|
||||
content_after_handler = self._extract_content_from_llm_response(
|
||||
resp_after_handler
|
||||
)
|
||||
if content_after_handler:
|
||||
self.observations.append(
|
||||
f"Response after tool execution: {content_after_handler}"
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"ReActAgent: LLM response after handler had no textual content."
|
||||
)
|
||||
|
||||
if log_context:
|
||||
log_context.stacks.append(
|
||||
{
|
||||
"component": "agent_tool_calls",
|
||||
"data": {"tool_calls": self.tool_calls.copy()},
|
||||
}
|
||||
)
|
||||
|
||||
yield {"sources": retrieved_data}
|
||||
|
||||
display_tool_calls = []
|
||||
for tc in self.tool_calls:
|
||||
cleaned_tc = tc.copy()
|
||||
if len(str(cleaned_tc.get("result", ""))) > 50:
|
||||
cleaned_tc["result"] = str(cleaned_tc["result"])[:50] + "..."
|
||||
display_tool_calls.append(cleaned_tc)
|
||||
if display_tool_calls:
|
||||
yield {"tool_calls": display_tool_calls}
|
||||
|
||||
if "SATISFIED" in content_after_handler:
|
||||
logger.info(
|
||||
"ReActAgent: LLM satisfied with the plan and data. Stopping reasoning."
|
||||
if not self.plan:
|
||||
logger.warning(
|
||||
f"ReActAgent: No plan generated in iteration {iteration}"
|
||||
)
|
||||
break
|
||||
self.observations.append(f"Plan (iteration {iteration}): {self.plan}")
|
||||
|
||||
# 3. Create Final Answer based on all observations
|
||||
final_answer_stream = self._create_final_answer(
|
||||
query, self.observations, log_context
|
||||
)
|
||||
for answer_chunk in final_answer_stream:
|
||||
yield {"answer": answer_chunk}
|
||||
logger.info("ReActAgent: Finished generating final answer.")
|
||||
satisfied = yield from self._execution_phase(query, tools_dict, log_context)
|
||||
|
||||
def _create_plan(
|
||||
self, query: str, docs_data: str, log_context: LogContext = None
|
||||
) -> Generator[str, None, None]:
|
||||
plan_prompt_filled = planning_prompt_template.replace("{query}", query)
|
||||
if "{summaries}" in plan_prompt_filled:
|
||||
summaries = docs_data if docs_data else "No documents retrieved."
|
||||
plan_prompt_filled = plan_prompt_filled.replace("{summaries}", summaries)
|
||||
plan_prompt_filled = plan_prompt_filled.replace("{prompt}", self.prompt or "")
|
||||
plan_prompt_filled = plan_prompt_filled.replace(
|
||||
"{observations}", "\n".join(self.observations)
|
||||
)
|
||||
if satisfied:
|
||||
logger.info("ReActAgent: Goal satisfied, stopping reasoning loop")
|
||||
break
|
||||
yield from self._synthesis_phase(query, log_context)
|
||||
|
||||
messages = [{"role": "user", "content": plan_prompt_filled}]
|
||||
def _reset_state(self):
|
||||
"""Reset agent state for new query"""
|
||||
self.plan = ""
|
||||
self.observations = []
|
||||
|
||||
plan_stream_from_llm = self.llm.gen_stream(
|
||||
def _planning_phase(
|
||||
self, query: str, log_context: LogContext
|
||||
) -> Generator[Dict, None, None]:
|
||||
"""Generate strategic plan for query"""
|
||||
logger.info("ReActAgent: Creating plan...")
|
||||
|
||||
plan_prompt = self._build_planning_prompt(query)
|
||||
messages = [{"role": "user", "content": plan_prompt}]
|
||||
|
||||
plan_stream = self.llm.gen_stream(
|
||||
model=self.gpt_model,
|
||||
messages=messages,
|
||||
tools=getattr(self, "tools", None), # Use self.tools
|
||||
tools=self.tools if self.tools else None,
|
||||
)
|
||||
|
||||
if log_context:
|
||||
data = build_stack_data(self.llm)
|
||||
log_context.stacks.append({"component": "planning_llm", "data": data})
|
||||
|
||||
for chunk in plan_stream_from_llm:
|
||||
content_piece = self._extract_content_from_llm_response(chunk)
|
||||
if content_piece:
|
||||
yield content_piece
|
||||
|
||||
def _create_final_answer(
|
||||
self, query: str, observations: List[str], log_context: LogContext = None
|
||||
) -> Generator[str, None, None]:
|
||||
observation_string = "\n".join(observations)
|
||||
max_obs_len = 10000
|
||||
if len(observation_string) > max_obs_len:
|
||||
observation_string = (
|
||||
observation_string[:max_obs_len] + "\n...[observations truncated]"
|
||||
)
|
||||
logger.warning(
|
||||
"ReActAgent: Truncated observations for final answer prompt due to length."
|
||||
log_context.stacks.append(
|
||||
{"component": "planning_llm", "data": build_stack_data(self.llm)}
|
||||
)
|
||||
plan_parts = []
|
||||
for chunk in plan_stream:
|
||||
content = self._extract_content(chunk)
|
||||
if content:
|
||||
plan_parts.append(content)
|
||||
yield {"thought": content}
|
||||
self.plan = "".join(plan_parts)
|
||||
|
||||
final_answer_prompt_filled = final_prompt_template.format(
|
||||
query=query, observations=observation_string
|
||||
def _execution_phase(
|
||||
self, query: str, tools_dict: Dict, log_context: LogContext
|
||||
) -> Generator[bool, None, None]:
|
||||
"""Execute plan with tool calls and observations"""
|
||||
execution_prompt = self._build_execution_prompt(query)
|
||||
messages = self._build_messages(execution_prompt, query)
|
||||
|
||||
llm_response = self._llm_gen(messages, log_context)
|
||||
initial_content = self._extract_content(llm_response)
|
||||
|
||||
if initial_content:
|
||||
self.observations.append(f"Initial response: {initial_content}")
|
||||
processed_response = self._llm_handler(
|
||||
llm_response, tools_dict, messages, log_context
|
||||
)
|
||||
|
||||
messages = [{"role": "user", "content": final_answer_prompt_filled}]
|
||||
for tool_call in self.tool_calls:
|
||||
observation = (
|
||||
f"Executed: {tool_call.get('tool_name', 'Unknown')} "
|
||||
f"with args {tool_call.get('arguments', {})}. "
|
||||
f"Result: {str(tool_call.get('result', ''))[:200]}"
|
||||
)
|
||||
self.observations.append(observation)
|
||||
final_content = self._extract_content(processed_response)
|
||||
if final_content:
|
||||
self.observations.append(f"Response after tools: {final_content}")
|
||||
if log_context:
|
||||
log_context.stacks.append(
|
||||
{
|
||||
"component": "agent_tool_calls",
|
||||
"data": {"tool_calls": self.tool_calls.copy()},
|
||||
}
|
||||
)
|
||||
yield {"sources": self.retrieved_docs}
|
||||
yield {"tool_calls": self._get_truncated_tool_calls()}
|
||||
|
||||
# Final answer should synthesize, not call tools.
|
||||
final_answer_stream_from_llm = self.llm.gen_stream(
|
||||
return "SATISFIED" in (final_content or "")
|
||||
|
||||
def _synthesis_phase(
|
||||
self, query: str, log_context: LogContext
|
||||
) -> Generator[Dict, None, None]:
|
||||
"""Synthesize final answer from all observations"""
|
||||
logger.info("ReActAgent: Generating final answer...")
|
||||
|
||||
final_prompt = self._build_final_answer_prompt(query)
|
||||
messages = [{"role": "user", "content": final_prompt}]
|
||||
|
||||
final_stream = self.llm.gen_stream(
|
||||
model=self.gpt_model, messages=messages, tools=None
|
||||
)
|
||||
if log_context:
|
||||
data = build_stack_data(self.llm)
|
||||
log_context.stacks.append({"component": "final_answer_llm", "data": data})
|
||||
|
||||
for chunk in final_answer_stream_from_llm:
|
||||
content_piece = self._extract_content_from_llm_response(chunk)
|
||||
if content_piece:
|
||||
yield content_piece
|
||||
if log_context:
|
||||
log_context.stacks.append(
|
||||
{"component": "final_answer_llm", "data": build_stack_data(self.llm)}
|
||||
)
|
||||
for chunk in final_stream:
|
||||
content = self._extract_content(chunk)
|
||||
if content:
|
||||
yield {"answer": content}
|
||||
|
||||
def _build_planning_prompt(self, query: str) -> str:
|
||||
"""Build planning phase prompt"""
|
||||
prompt = PLANNING_PROMPT_TEMPLATE.replace("{query}", query)
|
||||
prompt = prompt.replace("{prompt}", self.prompt or "")
|
||||
prompt = prompt.replace("{summaries}", "")
|
||||
prompt = prompt.replace("{observations}", "\n".join(self.observations))
|
||||
return prompt
|
||||
|
||||
def _build_execution_prompt(self, query: str) -> str:
|
||||
"""Build execution phase prompt with plan and observations"""
|
||||
observations_str = "\n".join(self.observations)
|
||||
|
||||
if len(observations_str) > 20000:
|
||||
observations_str = observations_str[:20000] + "\n...[truncated]"
|
||||
return (
|
||||
f"{self.prompt or ''}\n\n"
|
||||
f"Follow this plan:\n{self.plan}\n\n"
|
||||
f"Observations:\n{observations_str}\n\n"
|
||||
f"If sufficient data exists to answer '{query}', respond with 'SATISFIED'. "
|
||||
f"Otherwise, continue executing the plan."
|
||||
)
|
||||
|
||||
def _build_final_answer_prompt(self, query: str) -> str:
|
||||
"""Build final synthesis prompt"""
|
||||
observations_str = "\n".join(self.observations)
|
||||
|
||||
if len(observations_str) > 10000:
|
||||
observations_str = observations_str[:10000] + "\n...[truncated]"
|
||||
logger.warning("ReActAgent: Observations truncated for final answer")
|
||||
return FINAL_PROMPT_TEMPLATE.format(query=query, observations=observations_str)
|
||||
|
||||
def _extract_content(self, response: Any) -> str:
|
||||
"""Extract text content from various LLM response formats"""
|
||||
if not response:
|
||||
return ""
|
||||
collected = []
|
||||
|
||||
if isinstance(response, str):
|
||||
return response
|
||||
if hasattr(response, "message") and hasattr(response.message, "content"):
|
||||
if response.message.content:
|
||||
return response.message.content
|
||||
if hasattr(response, "choices") and response.choices:
|
||||
if hasattr(response.choices[0], "message"):
|
||||
content = response.choices[0].message.content
|
||||
if content:
|
||||
return content
|
||||
if hasattr(response, "content") and isinstance(response.content, list):
|
||||
if response.content and hasattr(response.content[0], "text"):
|
||||
return response.content[0].text
|
||||
try:
|
||||
for chunk in response:
|
||||
content_piece = ""
|
||||
|
||||
if hasattr(chunk, "choices") and chunk.choices:
|
||||
if hasattr(chunk.choices[0], "delta"):
|
||||
delta_content = chunk.choices[0].delta.content
|
||||
if delta_content:
|
||||
content_piece = delta_content
|
||||
elif hasattr(chunk, "type") and chunk.type == "content_block_delta":
|
||||
if hasattr(chunk, "delta") and hasattr(chunk.delta, "text"):
|
||||
content_piece = chunk.delta.text
|
||||
elif isinstance(chunk, str):
|
||||
content_piece = chunk
|
||||
if content_piece:
|
||||
collected.append(content_piece)
|
||||
except (TypeError, AttributeError):
|
||||
logger.debug(
|
||||
f"Response not iterable or unexpected format: {type(response)}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting content: {e}")
|
||||
return "".join(collected)
|
||||
|
||||
@@ -54,6 +54,10 @@ class AnswerResource(Resource, BaseAnswerResource):
|
||||
default=True,
|
||||
description="Whether to save the conversation",
|
||||
),
|
||||
"passthrough": fields.Raw(
|
||||
required=False,
|
||||
description="Dynamic parameters to inject into prompt template",
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -69,8 +73,17 @@ class AnswerResource(Resource, BaseAnswerResource):
|
||||
processor.initialize()
|
||||
if not processor.decoded_token:
|
||||
return make_response({"error": "Unauthorized"}, 401)
|
||||
agent = processor.create_agent()
|
||||
retriever = processor.create_retriever()
|
||||
|
||||
docs_together, docs_list = processor.pre_fetch_docs(
|
||||
data.get("question", "")
|
||||
)
|
||||
tools_data = processor.pre_fetch_tools()
|
||||
|
||||
agent = processor.create_agent(
|
||||
docs_together=docs_together,
|
||||
docs=docs_list,
|
||||
tools_data=tools_data,
|
||||
)
|
||||
|
||||
if error := self.check_usage(processor.agent_config):
|
||||
return error
|
||||
@@ -78,7 +91,6 @@ class AnswerResource(Resource, BaseAnswerResource):
|
||||
stream = self.complete_stream(
|
||||
question=data["question"],
|
||||
agent=agent,
|
||||
retriever=retriever,
|
||||
conversation_id=processor.conversation_id,
|
||||
user_api_key=processor.agent_config.get("user_api_key"),
|
||||
decoded_token=processor.decoded_token,
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
import logging
|
||||
from typing import Any, Dict, Generator, List, Optional
|
||||
|
||||
from flask import Response, make_response, jsonify
|
||||
from flask import jsonify, make_response, Response
|
||||
from flask_restx import Namespace
|
||||
|
||||
from application.api.answer.services.conversation_service import ConversationService
|
||||
@@ -41,9 +41,7 @@ class BaseAnswerResource:
|
||||
return missing_fields
|
||||
return None
|
||||
|
||||
def check_usage(
|
||||
self, agent_config: Dict
|
||||
) -> Optional[Response]:
|
||||
def check_usage(self, agent_config: Dict) -> Optional[Response]:
|
||||
"""Check if there is a usage limit and if it is exceeded
|
||||
|
||||
Args:
|
||||
@@ -51,30 +49,40 @@ class BaseAnswerResource:
|
||||
|
||||
Returns:
|
||||
None or Response if either of limits exceeded.
|
||||
|
||||
|
||||
"""
|
||||
api_key = agent_config.get("user_api_key")
|
||||
if not api_key:
|
||||
return None
|
||||
|
||||
|
||||
agents_collection = self.db["agents"]
|
||||
agent = agents_collection.find_one({"key": api_key})
|
||||
|
||||
if not agent:
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": "Invalid API key."
|
||||
}
|
||||
),
|
||||
401
|
||||
jsonify({"success": False, "message": "Invalid API key."}), 401
|
||||
)
|
||||
|
||||
limited_token_mode = agent.get("limited_token_mode", False)
|
||||
limited_request_mode = agent.get("limited_request_mode", False)
|
||||
token_limit = int(agent.get("token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]))
|
||||
request_limit = int(agent.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]))
|
||||
limited_token_mode_raw = agent.get("limited_token_mode", False)
|
||||
limited_request_mode_raw = agent.get("limited_request_mode", False)
|
||||
|
||||
limited_token_mode = (
|
||||
limited_token_mode_raw
|
||||
if isinstance(limited_token_mode_raw, bool)
|
||||
else limited_token_mode_raw == "True"
|
||||
)
|
||||
limited_request_mode = (
|
||||
limited_request_mode_raw
|
||||
if isinstance(limited_request_mode_raw, bool)
|
||||
else limited_request_mode_raw == "True"
|
||||
)
|
||||
|
||||
token_limit = int(
|
||||
agent.get("token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"])
|
||||
)
|
||||
request_limit = int(
|
||||
agent.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"])
|
||||
)
|
||||
|
||||
token_usage_collection = self.db["token_usage"]
|
||||
|
||||
@@ -83,18 +91,20 @@ class BaseAnswerResource:
|
||||
|
||||
match_query = {
|
||||
"timestamp": {"$gte": start_date, "$lte": end_date},
|
||||
"api_key": api_key
|
||||
"api_key": api_key,
|
||||
}
|
||||
|
||||
|
||||
if limited_token_mode:
|
||||
token_pipeline = [
|
||||
{"$match": match_query},
|
||||
{
|
||||
"$group": {
|
||||
"_id": None,
|
||||
"total_tokens": {"$sum": {"$add": ["$prompt_tokens", "$generated_tokens"]}}
|
||||
"total_tokens": {
|
||||
"$sum": {"$add": ["$prompt_tokens", "$generated_tokens"]}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
token_result = list(token_usage_collection.aggregate(token_pipeline))
|
||||
daily_token_usage = token_result[0]["total_tokens"] if token_result else 0
|
||||
@@ -108,26 +118,33 @@ class BaseAnswerResource:
|
||||
|
||||
if not limited_token_mode and not limited_request_mode:
|
||||
return None
|
||||
elif limited_token_mode and token_limit > daily_token_usage:
|
||||
return None
|
||||
elif limited_request_mode and request_limit > daily_request_usage:
|
||||
return None
|
||||
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": "Exceeding usage limit, please try again later."
|
||||
}
|
||||
),
|
||||
429, # too many requests
|
||||
token_exceeded = (
|
||||
limited_token_mode and token_limit > 0 and daily_token_usage >= token_limit
|
||||
)
|
||||
request_exceeded = (
|
||||
limited_request_mode
|
||||
and request_limit > 0
|
||||
and daily_request_usage >= request_limit
|
||||
)
|
||||
|
||||
if token_exceeded or request_exceeded:
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": "Exceeding usage limit, please try again later.",
|
||||
}
|
||||
),
|
||||
429,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def complete_stream(
|
||||
self,
|
||||
question: str,
|
||||
agent: Any,
|
||||
retriever: Any,
|
||||
conversation_id: Optional[str],
|
||||
user_api_key: Optional[str],
|
||||
decoded_token: Dict[str, Any],
|
||||
@@ -156,6 +173,7 @@ class BaseAnswerResource:
|
||||
agent_id: ID of agent used
|
||||
is_shared_usage: Flag for shared agent usage
|
||||
shared_token: Token for shared agent
|
||||
retrieved_docs: Pre-fetched documents for sources (optional)
|
||||
|
||||
Yields:
|
||||
Server-sent event strings
|
||||
@@ -166,7 +184,7 @@ class BaseAnswerResource:
|
||||
schema_info = None
|
||||
structured_chunks = []
|
||||
|
||||
for line in agent.gen(query=question, retriever=retriever):
|
||||
for line in agent.gen(query=question):
|
||||
if "answer" in line:
|
||||
response_full += str(line["answer"])
|
||||
if line.get("structured"):
|
||||
@@ -247,7 +265,6 @@ class BaseAnswerResource:
|
||||
data = json.dumps(id_data)
|
||||
yield f"data: {data}\n\n"
|
||||
|
||||
retriever_params = retriever.get_params()
|
||||
log_data = {
|
||||
"action": "stream_answer",
|
||||
"level": "info",
|
||||
@@ -256,7 +273,6 @@ class BaseAnswerResource:
|
||||
"question": question,
|
||||
"response": response_full,
|
||||
"sources": source_log_docs,
|
||||
"retriever_params": retriever_params,
|
||||
"attachments": attachment_ids,
|
||||
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
||||
}
|
||||
@@ -264,24 +280,19 @@ class BaseAnswerResource:
|
||||
log_data["structured_output"] = True
|
||||
if schema_info:
|
||||
log_data["schema"] = schema_info
|
||||
|
||||
# clean up text fields to be no longer than 10000 characters
|
||||
|
||||
# Clean up text fields to be no longer than 10000 characters
|
||||
for key, value in log_data.items():
|
||||
if isinstance(value, str) and len(value) > 10000:
|
||||
log_data[key] = value[:10000]
|
||||
|
||||
self.user_logs_collection.insert_one(log_data)
|
||||
|
||||
# End of stream
|
||||
self.user_logs_collection.insert_one(log_data)
|
||||
|
||||
data = json.dumps({"type": "end"})
|
||||
yield f"data: {data}\n\n"
|
||||
except GeneratorExit:
|
||||
# Client aborted the connection
|
||||
logger.info(
|
||||
f"Stream aborted by client for question: {question[:50]}... "
|
||||
)
|
||||
# Save partial response to database before exiting
|
||||
logger.info(f"Stream aborted by client for question: {question[:50]}... ")
|
||||
# Save partial response
|
||||
if should_save_conversation and response_full:
|
||||
try:
|
||||
if isNoneDoc:
|
||||
@@ -311,7 +322,9 @@ class BaseAnswerResource:
|
||||
attachment_ids=attachment_ids,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving partial response: {str(e)}", exc_info=True)
|
||||
logger.error(
|
||||
f"Error saving partial response: {str(e)}", exc_info=True
|
||||
)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error in stream: {str(e)}", exc_info=True)
|
||||
|
||||
@@ -60,6 +60,10 @@ class StreamResource(Resource, BaseAnswerResource):
|
||||
"attachments": fields.List(
|
||||
fields.String, required=False, description="List of attachment IDs"
|
||||
),
|
||||
"passthrough": fields.Raw(
|
||||
required=False,
|
||||
description="Dynamic parameters to inject into prompt template",
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -73,17 +77,20 @@ class StreamResource(Resource, BaseAnswerResource):
|
||||
processor = StreamProcessor(data, decoded_token)
|
||||
try:
|
||||
processor.initialize()
|
||||
agent = processor.create_agent()
|
||||
retriever = processor.create_retriever()
|
||||
|
||||
docs_together, docs_list = processor.pre_fetch_docs(data["question"])
|
||||
tools_data = processor.pre_fetch_tools()
|
||||
|
||||
agent = processor.create_agent(
|
||||
docs_together=docs_together, docs=docs_list, tools_data=tools_data
|
||||
)
|
||||
|
||||
if error := self.check_usage(processor.agent_config):
|
||||
return error
|
||||
|
||||
return Response(
|
||||
self.complete_stream(
|
||||
question=data["question"],
|
||||
agent=agent,
|
||||
retriever=retriever,
|
||||
conversation_id=processor.conversation_id,
|
||||
user_api_key=processor.agent_config.get("user_api_key"),
|
||||
decoded_token=processor.decoded_token,
|
||||
|
||||
@@ -133,10 +133,9 @@ class ConversationService:
|
||||
|
||||
messages_summary = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Summarise following conversation in no more than 3 "
|
||||
"words, respond ONLY with the summary, use the same "
|
||||
"language as the user query",
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant that creates concise conversation titles. "
|
||||
"Summarize conversations in 3 words or less using the same language as the user.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
|
||||
97
application/api/answer/services/prompt_renderer.py
Normal file
97
application/api/answer/services/prompt_renderer.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from application.templates.namespaces import NamespaceManager
|
||||
|
||||
from application.templates.template_engine import TemplateEngine, TemplateRenderError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PromptRenderer:
|
||||
"""Service for rendering prompts with dynamic context using namespaces"""
|
||||
|
||||
def __init__(self):
|
||||
self.template_engine = TemplateEngine()
|
||||
self.namespace_manager = NamespaceManager()
|
||||
|
||||
def render_prompt(
|
||||
self,
|
||||
prompt_content: str,
|
||||
user_id: Optional[str] = None,
|
||||
request_id: Optional[str] = None,
|
||||
passthrough_data: Optional[Dict[str, Any]] = None,
|
||||
docs: Optional[list] = None,
|
||||
docs_together: Optional[str] = None,
|
||||
tools_data: Optional[Dict[str, Any]] = None,
|
||||
**kwargs,
|
||||
) -> str:
|
||||
"""
|
||||
Render prompt with full context from all namespaces.
|
||||
|
||||
Args:
|
||||
prompt_content: Raw prompt template string
|
||||
user_id: Current user identifier
|
||||
request_id: Unique request identifier
|
||||
passthrough_data: Parameters from web request
|
||||
docs: RAG retrieved documents
|
||||
docs_together: Concatenated document content
|
||||
tools_data: Pre-fetched tool results organized by tool name
|
||||
**kwargs: Additional parameters for namespace builders
|
||||
|
||||
Returns:
|
||||
Rendered prompt string with all variables substituted
|
||||
|
||||
Raises:
|
||||
TemplateRenderError: If template rendering fails
|
||||
"""
|
||||
if not prompt_content:
|
||||
return ""
|
||||
|
||||
uses_template = self._uses_template_syntax(prompt_content)
|
||||
|
||||
if not uses_template:
|
||||
return self._apply_legacy_substitutions(prompt_content, docs_together)
|
||||
|
||||
try:
|
||||
context = self.namespace_manager.build_context(
|
||||
user_id=user_id,
|
||||
request_id=request_id,
|
||||
passthrough_data=passthrough_data,
|
||||
docs=docs,
|
||||
docs_together=docs_together,
|
||||
tools_data=tools_data,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return self.template_engine.render(prompt_content, context)
|
||||
except TemplateRenderError:
|
||||
raise
|
||||
except Exception as e:
|
||||
error_msg = f"Prompt rendering failed: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
raise TemplateRenderError(error_msg) from e
|
||||
|
||||
def _uses_template_syntax(self, prompt_content: str) -> bool:
|
||||
"""Check if prompt uses Jinja2 template syntax"""
|
||||
return "{{" in prompt_content and "}}" in prompt_content
|
||||
|
||||
def _apply_legacy_substitutions(
|
||||
self, prompt_content: str, docs_together: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
Apply backward-compatible substitutions for old prompt format.
|
||||
|
||||
Handles legacy {summaries} and {query} placeholders during transition period.
|
||||
"""
|
||||
if docs_together:
|
||||
prompt_content = prompt_content.replace("{summaries}", docs_together)
|
||||
return prompt_content
|
||||
|
||||
def validate_template(self, prompt_content: str) -> bool:
|
||||
"""Validate prompt template syntax"""
|
||||
return self.template_engine.validate_template(prompt_content)
|
||||
|
||||
def extract_variables(self, prompt_content: str) -> set[str]:
|
||||
"""Extract all variable names from prompt template"""
|
||||
return self.template_engine.extract_variables(prompt_content)
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, Optional, Set
|
||||
|
||||
from bson.dbref import DBRef
|
||||
|
||||
@@ -11,10 +11,15 @@ from bson.objectid import ObjectId
|
||||
|
||||
from application.agents.agent_creator import AgentCreator
|
||||
from application.api.answer.services.conversation_service import ConversationService
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
from application.core.mongo_db import MongoDB
|
||||
from application.core.settings import settings
|
||||
from application.retriever.retriever_creator import RetrieverCreator
|
||||
from application.utils import get_gpt_model, limit_chat_history
|
||||
from application.utils import (
|
||||
calculate_doc_token_budget,
|
||||
get_gpt_model,
|
||||
limit_chat_history,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -73,12 +78,16 @@ class StreamProcessor:
|
||||
self.all_sources = []
|
||||
self.attachments = []
|
||||
self.history = []
|
||||
self.retrieved_docs = []
|
||||
self.agent_config = {}
|
||||
self.retriever_config = {}
|
||||
self.is_shared_usage = False
|
||||
self.shared_token = None
|
||||
self.gpt_model = get_gpt_model()
|
||||
self.conversation_service = ConversationService()
|
||||
self.prompt_renderer = PromptRenderer()
|
||||
self._prompt_content: Optional[str] = None
|
||||
self._required_tool_actions: Optional[Dict[str, Set[Optional[str]]]] = None
|
||||
|
||||
def initialize(self):
|
||||
"""Initialize all required components for processing"""
|
||||
@@ -311,19 +320,312 @@ class StreamProcessor:
|
||||
)
|
||||
|
||||
def _configure_retriever(self):
|
||||
"""Configure the retriever based on request data"""
|
||||
history_token_limit = int(self.data.get("token_limit", 2000))
|
||||
doc_token_limit = calculate_doc_token_budget(
|
||||
gpt_model=self.gpt_model, history_token_limit=history_token_limit
|
||||
)
|
||||
|
||||
self.retriever_config = {
|
||||
"retriever_name": self.data.get("retriever", "classic"),
|
||||
"chunks": int(self.data.get("chunks", 2)),
|
||||
"token_limit": self.data.get("token_limit", settings.DEFAULT_MAX_HISTORY),
|
||||
"doc_token_limit": doc_token_limit,
|
||||
"history_token_limit": history_token_limit,
|
||||
}
|
||||
|
||||
api_key = self.data.get("api_key") or self.agent_key
|
||||
if not api_key and "isNoneDoc" in self.data and self.data["isNoneDoc"]:
|
||||
self.retriever_config["chunks"] = 0
|
||||
|
||||
def create_agent(self):
|
||||
"""Create and return the configured agent"""
|
||||
def create_retriever(self):
|
||||
return RetrieverCreator.create_retriever(
|
||||
self.retriever_config["retriever_name"],
|
||||
source=self.source,
|
||||
chat_history=self.history,
|
||||
prompt=get_prompt(self.agent_config["prompt_id"], self.prompts_collection),
|
||||
chunks=self.retriever_config["chunks"],
|
||||
doc_token_limit=self.retriever_config.get("doc_token_limit", 50000),
|
||||
gpt_model=self.gpt_model,
|
||||
user_api_key=self.agent_config["user_api_key"],
|
||||
decoded_token=self.decoded_token,
|
||||
)
|
||||
|
||||
def pre_fetch_docs(self, question: str) -> tuple[Optional[str], Optional[list]]:
|
||||
"""Pre-fetch documents for template rendering before agent creation"""
|
||||
if self.data.get("isNoneDoc", False):
|
||||
logger.info("Pre-fetch skipped: isNoneDoc=True")
|
||||
return None, None
|
||||
try:
|
||||
retriever = self.create_retriever()
|
||||
logger.info(
|
||||
f"Pre-fetching docs with chunks={retriever.chunks}, doc_token_limit={retriever.doc_token_limit}"
|
||||
)
|
||||
docs = retriever.search(question)
|
||||
logger.info(f"Pre-fetch retrieved {len(docs) if docs else 0} documents")
|
||||
|
||||
if not docs:
|
||||
logger.info("Pre-fetch: No documents returned from search")
|
||||
return None, None
|
||||
self.retrieved_docs = docs
|
||||
|
||||
docs_with_filenames = []
|
||||
for doc in docs:
|
||||
filename = doc.get("filename") or doc.get("title") or doc.get("source")
|
||||
if filename:
|
||||
chunk_header = str(filename)
|
||||
docs_with_filenames.append(f"{chunk_header}\n{doc['text']}")
|
||||
else:
|
||||
docs_with_filenames.append(doc["text"])
|
||||
docs_together = "\n\n".join(docs_with_filenames)
|
||||
|
||||
logger.info(f"Pre-fetch docs_together size: {len(docs_together)} chars")
|
||||
|
||||
return docs_together, docs
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to pre-fetch docs: {str(e)}", exc_info=True)
|
||||
return None, None
|
||||
|
||||
def pre_fetch_tools(self) -> Optional[Dict[str, Any]]:
|
||||
"""Pre-fetch tool data for template rendering before agent creation
|
||||
|
||||
Can be controlled via:
|
||||
1. Global setting: ENABLE_TOOL_PREFETCH in .env
|
||||
2. Per-request: disable_tool_prefetch in request data
|
||||
"""
|
||||
if not settings.ENABLE_TOOL_PREFETCH:
|
||||
logger.info(
|
||||
"Tool pre-fetching disabled globally via ENABLE_TOOL_PREFETCH setting"
|
||||
)
|
||||
return None
|
||||
|
||||
if self.data.get("disable_tool_prefetch", False):
|
||||
logger.info("Tool pre-fetching disabled for this request")
|
||||
return None
|
||||
|
||||
required_tool_actions = self._get_required_tool_actions()
|
||||
filtering_enabled = required_tool_actions is not None
|
||||
|
||||
try:
|
||||
user_tools_collection = self.db["user_tools"]
|
||||
user_id = self.initial_user_id or "local"
|
||||
|
||||
user_tools = list(
|
||||
user_tools_collection.find({"user": user_id, "status": True})
|
||||
)
|
||||
|
||||
if not user_tools:
|
||||
return None
|
||||
|
||||
tools_data = {}
|
||||
|
||||
for tool_doc in user_tools:
|
||||
tool_name = tool_doc.get("name")
|
||||
tool_id = str(tool_doc.get("_id"))
|
||||
|
||||
if filtering_enabled:
|
||||
required_actions_by_name = required_tool_actions.get(
|
||||
tool_name, set()
|
||||
)
|
||||
required_actions_by_id = required_tool_actions.get(tool_id, set())
|
||||
|
||||
required_actions = required_actions_by_name | required_actions_by_id
|
||||
|
||||
if not required_actions:
|
||||
continue
|
||||
else:
|
||||
required_actions = None
|
||||
|
||||
tool_data = self._fetch_tool_data(tool_doc, required_actions)
|
||||
if tool_data:
|
||||
tools_data[tool_name] = tool_data
|
||||
tools_data[tool_id] = tool_data
|
||||
|
||||
return tools_data if tools_data else None
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to pre-fetch tools: {type(e).__name__}")
|
||||
return None
|
||||
|
||||
def _fetch_tool_data(
|
||||
self,
|
||||
tool_doc: Dict[str, Any],
|
||||
required_actions: Optional[Set[Optional[str]]],
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch and execute tool actions with saved parameters"""
|
||||
try:
|
||||
from application.agents.tools.tool_manager import ToolManager
|
||||
|
||||
tool_name = tool_doc.get("name")
|
||||
tool_config = tool_doc.get("config", {}).copy()
|
||||
tool_config["tool_id"] = str(tool_doc["_id"])
|
||||
|
||||
tool_manager = ToolManager(config={tool_name: tool_config})
|
||||
user_id = self.initial_user_id or "local"
|
||||
tool = tool_manager.load_tool(tool_name, tool_config, user_id=user_id)
|
||||
|
||||
if not tool:
|
||||
logger.debug(f"Tool '{tool_name}' failed to load")
|
||||
return None
|
||||
|
||||
tool_actions = tool.get_actions_metadata()
|
||||
if not tool_actions:
|
||||
logger.debug(f"Tool '{tool_name}' has no actions")
|
||||
return None
|
||||
|
||||
saved_actions = tool_doc.get("actions", [])
|
||||
|
||||
include_all_actions = required_actions is None or (
|
||||
required_actions and None in required_actions
|
||||
)
|
||||
allowed_actions: Set[str] = (
|
||||
{action for action in required_actions if isinstance(action, str)}
|
||||
if required_actions
|
||||
else set()
|
||||
)
|
||||
|
||||
action_results = {}
|
||||
for action_meta in tool_actions:
|
||||
action_name = action_meta.get("name")
|
||||
if action_name is None:
|
||||
continue
|
||||
if (
|
||||
not include_all_actions
|
||||
and allowed_actions
|
||||
and action_name not in allowed_actions
|
||||
):
|
||||
continue
|
||||
|
||||
try:
|
||||
saved_action = None
|
||||
for sa in saved_actions:
|
||||
if sa.get("name") == action_name:
|
||||
saved_action = sa
|
||||
break
|
||||
|
||||
action_params = action_meta.get("parameters", {})
|
||||
properties = action_params.get("properties", {})
|
||||
|
||||
kwargs = {}
|
||||
for param_name, param_spec in properties.items():
|
||||
if saved_action:
|
||||
saved_props = saved_action.get("parameters", {}).get(
|
||||
"properties", {}
|
||||
)
|
||||
if param_name in saved_props:
|
||||
param_value = saved_props[param_name].get("value")
|
||||
if param_value is not None:
|
||||
kwargs[param_name] = param_value
|
||||
continue
|
||||
|
||||
if param_name in tool_config:
|
||||
kwargs[param_name] = tool_config[param_name]
|
||||
elif "default" in param_spec:
|
||||
kwargs[param_name] = param_spec["default"]
|
||||
|
||||
result = tool.execute_action(action_name, **kwargs)
|
||||
action_results[action_name] = result
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
f"Action '{action_name}' execution failed: {type(e).__name__}"
|
||||
)
|
||||
continue
|
||||
|
||||
return action_results if action_results else None
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Tool pre-fetch failed for '{tool_name}': {type(e).__name__}")
|
||||
return None
|
||||
|
||||
def _get_prompt_content(self) -> Optional[str]:
|
||||
"""Retrieve and cache the raw prompt content for the current agent configuration."""
|
||||
if self._prompt_content is not None:
|
||||
return self._prompt_content
|
||||
prompt_id = (
|
||||
self.agent_config.get("prompt_id")
|
||||
if isinstance(self.agent_config, dict)
|
||||
else None
|
||||
)
|
||||
if not prompt_id:
|
||||
return None
|
||||
try:
|
||||
self._prompt_content = get_prompt(prompt_id, self.prompts_collection)
|
||||
except ValueError as e:
|
||||
logger.debug(f"Invalid prompt ID '{prompt_id}': {str(e)}")
|
||||
self._prompt_content = None
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to fetch prompt '{prompt_id}': {type(e).__name__}")
|
||||
self._prompt_content = None
|
||||
return self._prompt_content
|
||||
|
||||
def _get_required_tool_actions(self) -> Optional[Dict[str, Set[Optional[str]]]]:
|
||||
"""Determine which tool actions are referenced in the prompt template"""
|
||||
if self._required_tool_actions is not None:
|
||||
return self._required_tool_actions
|
||||
|
||||
prompt_content = self._get_prompt_content()
|
||||
if prompt_content is None:
|
||||
return None
|
||||
|
||||
if "{{" not in prompt_content or "}}" not in prompt_content:
|
||||
self._required_tool_actions = {}
|
||||
return self._required_tool_actions
|
||||
|
||||
try:
|
||||
from application.templates.template_engine import TemplateEngine
|
||||
|
||||
template_engine = TemplateEngine()
|
||||
usages = template_engine.extract_tool_usages(prompt_content)
|
||||
self._required_tool_actions = usages
|
||||
return self._required_tool_actions
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to extract tool usages: {type(e).__name__}")
|
||||
self._required_tool_actions = {}
|
||||
return self._required_tool_actions
|
||||
|
||||
def _fetch_memory_tool_data(
|
||||
self, tool_doc: Dict[str, Any]
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch memory tool data for pre-injection into prompt"""
|
||||
try:
|
||||
tool_config = tool_doc.get("config", {}).copy()
|
||||
tool_config["tool_id"] = str(tool_doc["_id"])
|
||||
|
||||
from application.agents.tools.memory import MemoryTool
|
||||
|
||||
memory_tool = MemoryTool(tool_config, self.initial_user_id)
|
||||
|
||||
root_view = memory_tool.execute_action("view", path="/")
|
||||
|
||||
if "Error:" in root_view or not root_view.strip():
|
||||
return None
|
||||
|
||||
return {"root": root_view, "available": True}
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch memory tool data: {str(e)}")
|
||||
return None
|
||||
|
||||
def create_agent(
|
||||
self,
|
||||
docs_together: Optional[str] = None,
|
||||
docs: Optional[list] = None,
|
||||
tools_data: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""Create and return the configured agent with rendered prompt"""
|
||||
raw_prompt = self._get_prompt_content()
|
||||
if raw_prompt is None:
|
||||
raw_prompt = get_prompt(
|
||||
self.agent_config["prompt_id"], self.prompts_collection
|
||||
)
|
||||
self._prompt_content = raw_prompt
|
||||
|
||||
rendered_prompt = self.prompt_renderer.render_prompt(
|
||||
prompt_content=raw_prompt,
|
||||
user_id=self.initial_user_id,
|
||||
request_id=self.data.get("request_id"),
|
||||
passthrough_data=self.data.get("passthrough"),
|
||||
docs=docs,
|
||||
docs_together=docs_together,
|
||||
tools_data=tools_data,
|
||||
)
|
||||
|
||||
return AgentCreator.create_agent(
|
||||
self.agent_config["agent_type"],
|
||||
endpoint="stream",
|
||||
@@ -331,23 +633,10 @@ class StreamProcessor:
|
||||
gpt_model=self.gpt_model,
|
||||
api_key=settings.API_KEY,
|
||||
user_api_key=self.agent_config["user_api_key"],
|
||||
prompt=get_prompt(self.agent_config["prompt_id"], self.prompts_collection),
|
||||
prompt=rendered_prompt,
|
||||
chat_history=self.history,
|
||||
retrieved_docs=self.retrieved_docs,
|
||||
decoded_token=self.decoded_token,
|
||||
attachments=self.attachments,
|
||||
json_schema=self.agent_config.get("json_schema"),
|
||||
)
|
||||
|
||||
def create_retriever(self):
|
||||
"""Create and return the configured retriever"""
|
||||
return RetrieverCreator.create_retriever(
|
||||
self.retriever_config["retriever_name"],
|
||||
source=self.source,
|
||||
chat_history=self.history,
|
||||
prompt=get_prompt(self.agent_config["prompt_id"], self.prompts_collection),
|
||||
chunks=self.retriever_config["chunks"],
|
||||
token_limit=self.retriever_config["token_limit"],
|
||||
gpt_model=self.gpt_model,
|
||||
user_api_key=self.agent_config["user_api_key"],
|
||||
decoded_token=self.decoded_token,
|
||||
)
|
||||
|
||||
@@ -10,7 +10,6 @@ from flask import current_app, jsonify, make_response, request
|
||||
from flask_restx import fields, Namespace, Resource
|
||||
|
||||
from application.api import api
|
||||
from application.core.settings import settings
|
||||
from application.api.user.base import (
|
||||
agents_collection,
|
||||
db,
|
||||
@@ -20,6 +19,7 @@ from application.api.user.base import (
|
||||
storage,
|
||||
users_collection,
|
||||
)
|
||||
from application.core.settings import settings
|
||||
from application.utils import (
|
||||
check_required_fields,
|
||||
generate_image_url,
|
||||
@@ -76,9 +76,13 @@ class GetAgent(Resource):
|
||||
"status": agent.get("status", ""),
|
||||
"json_schema": agent.get("json_schema"),
|
||||
"limited_token_mode": agent.get("limited_token_mode", False),
|
||||
"token_limit": agent.get("token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]),
|
||||
"token_limit": agent.get(
|
||||
"token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]
|
||||
),
|
||||
"limited_request_mode": agent.get("limited_request_mode", False),
|
||||
"request_limit": agent.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]),
|
||||
"request_limit": agent.get(
|
||||
"request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]
|
||||
),
|
||||
"created_at": agent.get("createdAt", ""),
|
||||
"updated_at": agent.get("updatedAt", ""),
|
||||
"last_used_at": agent.get("lastUsedAt", ""),
|
||||
@@ -149,9 +153,13 @@ class GetAgents(Resource):
|
||||
"status": agent.get("status", ""),
|
||||
"json_schema": agent.get("json_schema"),
|
||||
"limited_token_mode": agent.get("limited_token_mode", False),
|
||||
"token_limit": agent.get("token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]),
|
||||
"token_limit": agent.get(
|
||||
"token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]
|
||||
),
|
||||
"limited_request_mode": agent.get("limited_request_mode", False),
|
||||
"request_limit": agent.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]),
|
||||
"request_limit": agent.get(
|
||||
"request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]
|
||||
),
|
||||
"created_at": agent.get("createdAt", ""),
|
||||
"updated_at": agent.get("updatedAt", ""),
|
||||
"last_used_at": agent.get("lastUsedAt", ""),
|
||||
@@ -209,21 +217,19 @@ class CreateAgent(Resource):
|
||||
description="JSON schema for enforcing structured output format",
|
||||
),
|
||||
"limited_token_mode": fields.Boolean(
|
||||
required=False,
|
||||
description="Whether the agent is in limited token mode"
|
||||
required=False, description="Whether the agent is in limited token mode"
|
||||
),
|
||||
"token_limit": fields.Integer(
|
||||
required=False,
|
||||
description="Token limit for the agent in limited mode"
|
||||
required=False, description="Token limit for the agent in limited mode"
|
||||
),
|
||||
"limited_request_mode": fields.Boolean(
|
||||
required=False,
|
||||
description="Whether the agent is in limited request mode"
|
||||
description="Whether the agent is in limited request mode",
|
||||
),
|
||||
"request_limit": fields.Integer(
|
||||
required=False,
|
||||
description="Request limit for the agent in limited mode"
|
||||
)
|
||||
description="Request limit for the agent in limited mode",
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -369,10 +375,26 @@ class CreateAgent(Resource):
|
||||
"agent_type": data.get("agent_type", ""),
|
||||
"status": data.get("status"),
|
||||
"json_schema": data.get("json_schema"),
|
||||
"limited_token_mode": data.get("limited_token_mode", False),
|
||||
"token_limit": data.get("token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]),
|
||||
"limited_request_mode": data.get("limited_request_mode", False),
|
||||
"request_limit": data.get("request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]),
|
||||
"limited_token_mode": (
|
||||
data.get("limited_token_mode") == "True"
|
||||
if isinstance(data.get("limited_token_mode"), str)
|
||||
else bool(data.get("limited_token_mode", False))
|
||||
),
|
||||
"token_limit": int(
|
||||
data.get(
|
||||
"token_limit", settings.DEFAULT_AGENT_LIMITS["token_limit"]
|
||||
)
|
||||
),
|
||||
"limited_request_mode": (
|
||||
data.get("limited_request_mode") == "True"
|
||||
if isinstance(data.get("limited_request_mode"), str)
|
||||
else bool(data.get("limited_request_mode", False))
|
||||
),
|
||||
"request_limit": int(
|
||||
data.get(
|
||||
"request_limit", settings.DEFAULT_AGENT_LIMITS["request_limit"]
|
||||
)
|
||||
),
|
||||
"createdAt": datetime.datetime.now(datetime.timezone.utc),
|
||||
"updatedAt": datetime.datetime.now(datetime.timezone.utc),
|
||||
"lastUsedAt": None,
|
||||
@@ -429,21 +451,19 @@ class UpdateAgent(Resource):
|
||||
description="JSON schema for enforcing structured output format",
|
||||
),
|
||||
"limited_token_mode": fields.Boolean(
|
||||
required=False,
|
||||
description="Whether the agent is in limited token mode"
|
||||
required=False, description="Whether the agent is in limited token mode"
|
||||
),
|
||||
"token_limit": fields.Integer(
|
||||
required=False,
|
||||
description="Token limit for the agent in limited mode"
|
||||
required=False, description="Token limit for the agent in limited mode"
|
||||
),
|
||||
"limited_request_mode": fields.Boolean(
|
||||
require=False,
|
||||
description="Whether the agent is in limited request mode"
|
||||
description="Whether the agent is in limited request mode",
|
||||
),
|
||||
"request_limit": fields.Integer(
|
||||
required=False,
|
||||
description="Request limit for the agent in limited mode"
|
||||
)
|
||||
description="Request limit for the agent in limited mode",
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -534,7 +554,7 @@ class UpdateAgent(Resource):
|
||||
"limited_token_mode",
|
||||
"token_limit",
|
||||
"limited_request_mode",
|
||||
"request_limit"
|
||||
"request_limit",
|
||||
]
|
||||
|
||||
for field in allowed_fields:
|
||||
@@ -652,8 +672,15 @@ class UpdateAgent(Resource):
|
||||
else:
|
||||
update_fields[field] = None
|
||||
elif field == "limited_token_mode":
|
||||
is_mode_enabled = data.get("limited_token_mode", False)
|
||||
if is_mode_enabled and data.get("token_limit") is None:
|
||||
raw_value = data.get("limited_token_mode", False)
|
||||
bool_value = (
|
||||
raw_value == "True"
|
||||
if isinstance(raw_value, str)
|
||||
else bool(raw_value)
|
||||
)
|
||||
update_fields[field] = bool_value
|
||||
|
||||
if bool_value and data.get("token_limit") is None:
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
@@ -664,8 +691,15 @@ class UpdateAgent(Resource):
|
||||
400,
|
||||
)
|
||||
elif field == "limited_request_mode":
|
||||
is_mode_enabled = data.get("limited_request_mode", False)
|
||||
if is_mode_enabled and data.get("request_limit") is None:
|
||||
raw_value = data.get("limited_request_mode", False)
|
||||
bool_value = (
|
||||
raw_value == "True"
|
||||
if isinstance(raw_value, str)
|
||||
else bool(raw_value)
|
||||
)
|
||||
update_fields[field] = bool_value
|
||||
|
||||
if bool_value and data.get("request_limit") is None:
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
@@ -677,7 +711,11 @@ class UpdateAgent(Resource):
|
||||
)
|
||||
elif field == "token_limit":
|
||||
token_limit = data.get("token_limit")
|
||||
if token_limit is not None and not data.get("limited_token_mode"):
|
||||
# Convert to int and store
|
||||
update_fields[field] = int(token_limit) if token_limit else 0
|
||||
|
||||
# Validate consistency with mode
|
||||
if update_fields[field] > 0 and not data.get("limited_token_mode"):
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
@@ -689,7 +727,9 @@ class UpdateAgent(Resource):
|
||||
)
|
||||
elif field == "request_limit":
|
||||
request_limit = data.get("request_limit")
|
||||
if request_limit is not None and not data.get("limited_request_mode"):
|
||||
update_fields[field] = int(request_limit) if request_limit else 0
|
||||
|
||||
if update_fields[field] > 0 and not data.get("limited_request_mode"):
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@ class StoreAttachment(Resource):
|
||||
api.model(
|
||||
"AttachmentModel",
|
||||
{
|
||||
"file": fields.Raw(required=True, description="File to upload"),
|
||||
"file": fields.Raw(required=True, description="File(s) to upload"),
|
||||
"api_key": fields.String(
|
||||
required=False, description="API key (optional)"
|
||||
),
|
||||
@@ -33,18 +33,24 @@ class StoreAttachment(Resource):
|
||||
)
|
||||
)
|
||||
@api.doc(
|
||||
description="Stores a single attachment without vectorization or training. Supports user or API key authentication."
|
||||
description="Stores one or multiple attachments without vectorization or training. Supports user or API key authentication."
|
||||
)
|
||||
def post(self):
|
||||
decoded_token = getattr(request, "decoded_token", None)
|
||||
api_key = request.form.get("api_key") or request.args.get("api_key")
|
||||
file = request.files.get("file")
|
||||
|
||||
if not file or file.filename == "":
|
||||
|
||||
files = request.files.getlist("file")
|
||||
if not files:
|
||||
single_file = request.files.get("file")
|
||||
if single_file:
|
||||
files = [single_file]
|
||||
|
||||
if not files or all(f.filename == "" for f in files):
|
||||
return make_response(
|
||||
jsonify({"status": "error", "message": "Missing file"}),
|
||||
jsonify({"status": "error", "message": "Missing file(s)"}),
|
||||
400,
|
||||
)
|
||||
|
||||
user = None
|
||||
if decoded_token:
|
||||
user = safe_filename(decoded_token.get("sub"))
|
||||
@@ -59,32 +65,74 @@ class StoreAttachment(Resource):
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Authentication required"}), 401
|
||||
)
|
||||
|
||||
try:
|
||||
attachment_id = ObjectId()
|
||||
original_filename = safe_filename(os.path.basename(file.filename))
|
||||
relative_path = f"{settings.UPLOAD_FOLDER}/{user}/attachments/{str(attachment_id)}/{original_filename}"
|
||||
tasks = []
|
||||
errors = []
|
||||
original_file_count = len(files)
|
||||
|
||||
for idx, file in enumerate(files):
|
||||
try:
|
||||
attachment_id = ObjectId()
|
||||
original_filename = safe_filename(os.path.basename(file.filename))
|
||||
relative_path = f"{settings.UPLOAD_FOLDER}/{user}/attachments/{str(attachment_id)}/{original_filename}"
|
||||
|
||||
metadata = storage.save_file(file, relative_path)
|
||||
|
||||
file_info = {
|
||||
"filename": original_filename,
|
||||
"attachment_id": str(attachment_id),
|
||||
"path": relative_path,
|
||||
"metadata": metadata,
|
||||
}
|
||||
|
||||
task = store_attachment.delay(file_info, user)
|
||||
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": True,
|
||||
"task_id": task.id,
|
||||
"message": "File uploaded successfully. Processing started.",
|
||||
metadata = storage.save_file(file, relative_path)
|
||||
file_info = {
|
||||
"filename": original_filename,
|
||||
"attachment_id": str(attachment_id),
|
||||
"path": relative_path,
|
||||
"metadata": metadata,
|
||||
}
|
||||
),
|
||||
200,
|
||||
)
|
||||
|
||||
task = store_attachment.delay(file_info, user)
|
||||
tasks.append({
|
||||
"task_id": task.id,
|
||||
"filename": original_filename,
|
||||
"attachment_id": str(attachment_id),
|
||||
})
|
||||
except Exception as file_err:
|
||||
current_app.logger.error(f"Error processing file {idx} ({file.filename}): {file_err}", exc_info=True)
|
||||
errors.append({
|
||||
"filename": file.filename,
|
||||
"error": str(file_err)
|
||||
})
|
||||
|
||||
if not tasks:
|
||||
error_msg = "No valid files to upload"
|
||||
if errors:
|
||||
error_msg += f". Errors: {errors}"
|
||||
return make_response(
|
||||
jsonify({"status": "error", "message": error_msg, "errors": errors}),
|
||||
400,
|
||||
)
|
||||
|
||||
if original_file_count == 1 and len(tasks) == 1:
|
||||
current_app.logger.info("Returning single task_id response")
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": True,
|
||||
"task_id": tasks[0]["task_id"],
|
||||
"message": "File uploaded successfully. Processing started.",
|
||||
}
|
||||
),
|
||||
200,
|
||||
)
|
||||
else:
|
||||
response_data = {
|
||||
"success": True,
|
||||
"tasks": tasks,
|
||||
"message": f"{len(tasks)} file(s) uploaded successfully. Processing started.",
|
||||
}
|
||||
if errors:
|
||||
response_data["errors"] = errors
|
||||
response_data["message"] += f" {len(errors)} file(s) failed."
|
||||
|
||||
return make_response(
|
||||
jsonify(response_data),
|
||||
200,
|
||||
)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error storing attachment: {err}", exc_info=True)
|
||||
return make_response(jsonify({"success": False, "error": str(err)}), 400)
|
||||
|
||||
@@ -13,7 +13,6 @@ from application.api.user.base import (
|
||||
agents_collection,
|
||||
attachments_collection,
|
||||
conversations_collection,
|
||||
db,
|
||||
shared_conversations_collections,
|
||||
)
|
||||
from application.utils import check_required_fields
|
||||
@@ -97,9 +96,7 @@ class ShareConversation(Resource):
|
||||
api_uuid = pre_existing_api_document["key"]
|
||||
pre_existing = shared_conversations_collections.find_one(
|
||||
{
|
||||
"conversation_id": DBRef(
|
||||
"conversations", ObjectId(conversation_id)
|
||||
),
|
||||
"conversation_id": ObjectId(conversation_id),
|
||||
"isPromptable": is_promptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
@@ -120,10 +117,7 @@ class ShareConversation(Resource):
|
||||
shared_conversations_collections.insert_one(
|
||||
{
|
||||
"uuid": explicit_binary,
|
||||
"conversation_id": {
|
||||
"$ref": "conversations",
|
||||
"$id": ObjectId(conversation_id),
|
||||
},
|
||||
"conversation_id": ObjectId(conversation_id),
|
||||
"isPromptable": is_promptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
@@ -154,10 +148,7 @@ class ShareConversation(Resource):
|
||||
shared_conversations_collections.insert_one(
|
||||
{
|
||||
"uuid": explicit_binary,
|
||||
"conversation_id": {
|
||||
"$ref": "conversations",
|
||||
"$id": ObjectId(conversation_id),
|
||||
},
|
||||
"conversation_id": ObjectId(conversation_id),
|
||||
"isPromptable": is_promptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
@@ -175,9 +166,7 @@ class ShareConversation(Resource):
|
||||
)
|
||||
pre_existing = shared_conversations_collections.find_one(
|
||||
{
|
||||
"conversation_id": DBRef(
|
||||
"conversations", ObjectId(conversation_id)
|
||||
),
|
||||
"conversation_id": ObjectId(conversation_id),
|
||||
"isPromptable": is_promptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
@@ -197,10 +186,7 @@ class ShareConversation(Resource):
|
||||
shared_conversations_collections.insert_one(
|
||||
{
|
||||
"uuid": explicit_binary,
|
||||
"conversation_id": {
|
||||
"$ref": "conversations",
|
||||
"$id": ObjectId(conversation_id),
|
||||
},
|
||||
"conversation_id": ObjectId(conversation_id),
|
||||
"isPromptable": is_promptable,
|
||||
"first_n_queries": current_n_queries,
|
||||
"user": user,
|
||||
@@ -233,10 +219,12 @@ class GetPubliclySharedConversations(Resource):
|
||||
if (
|
||||
shared
|
||||
and "conversation_id" in shared
|
||||
and isinstance(shared["conversation_id"], DBRef)
|
||||
):
|
||||
conversation_ref = shared["conversation_id"]
|
||||
conversation = db.dereference(conversation_ref)
|
||||
# conversation_id is now stored as an ObjectId, not a DBRef
|
||||
conversation_id = shared["conversation_id"]
|
||||
conversation = conversations_collection.find_one(
|
||||
{"_id": conversation_id}
|
||||
)
|
||||
if conversation is None:
|
||||
return make_response(
|
||||
jsonify(
|
||||
|
||||
@@ -56,9 +56,10 @@ class GetTools(Resource):
|
||||
tools = user_tools_collection.find({"user": user})
|
||||
user_tools = []
|
||||
for tool in tools:
|
||||
tool["id"] = str(tool["_id"])
|
||||
tool.pop("_id")
|
||||
user_tools.append(tool)
|
||||
tool_copy = {**tool}
|
||||
tool_copy["id"] = str(tool["_id"])
|
||||
tool_copy.pop("_id", None)
|
||||
user_tools.append(tool_copy)
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error getting user tools: {err}", exc_info=True)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
|
||||
@@ -23,10 +23,18 @@ class Settings(BaseSettings):
|
||||
LLM_PATH: str = os.path.join(current_dir, "models/docsgpt-7b-f16.gguf")
|
||||
DEFAULT_MAX_HISTORY: int = 150
|
||||
LLM_TOKEN_LIMITS: dict = {
|
||||
"gpt-4o": 128000,
|
||||
"gpt-4o-mini": 128000,
|
||||
"gpt-4": 8192,
|
||||
"gpt-3.5-turbo": 4096,
|
||||
"claude-2": 1e5,
|
||||
"gemini-2.5-flash": 1e6,
|
||||
"claude-2": int(1e5),
|
||||
"gemini-2.5-flash": int(1e6),
|
||||
}
|
||||
DEFAULT_LLM_TOKEN_LIMIT: int = 128000
|
||||
RESERVED_TOKENS: dict = {
|
||||
"system_prompt": 500,
|
||||
"current_query": 500,
|
||||
"safety_buffer": 1000,
|
||||
}
|
||||
DEFAULT_AGENT_LIMITS: dict = {
|
||||
"token_limit": 50000,
|
||||
@@ -133,5 +141,8 @@ class Settings(BaseSettings):
|
||||
TTS_PROVIDER: str = "google_tts" # google_tts or elevenlabs
|
||||
ELEVENLABS_API_KEY: Optional[str] = None
|
||||
|
||||
# Tool pre-fetch settings
|
||||
ENABLE_TOOL_PREFETCH: bool = True
|
||||
|
||||
path = Path(__file__).parent.parent.absolute()
|
||||
settings = Settings(_env_file=path.joinpath(".env"), _env_file_encoding="utf-8")
|
||||
|
||||
@@ -44,6 +44,12 @@ class BaseLLM(ABC):
|
||||
)
|
||||
return self._fallback_llm
|
||||
|
||||
@staticmethod
|
||||
def _remove_null_values(args_dict):
|
||||
if not isinstance(args_dict, dict):
|
||||
return args_dict
|
||||
return {k: v for k, v in args_dict.items() if v is not None}
|
||||
|
||||
def _execute_with_fallback(
|
||||
self, method_name: str, decorators: list, *args, **kwargs
|
||||
):
|
||||
|
||||
@@ -33,14 +33,15 @@ class DocsGPTAPILLM(BaseLLM):
|
||||
{"role": role, "content": item["text"]}
|
||||
)
|
||||
elif "function_call" in item:
|
||||
cleaned_args = self._remove_null_values(
|
||||
item["function_call"]["args"]
|
||||
)
|
||||
tool_call = {
|
||||
"id": item["function_call"]["call_id"],
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": item["function_call"]["name"],
|
||||
"arguments": json.dumps(
|
||||
item["function_call"]["args"]
|
||||
),
|
||||
"arguments": json.dumps(cleaned_args),
|
||||
},
|
||||
}
|
||||
cleaned_messages.append(
|
||||
|
||||
@@ -163,10 +163,14 @@ class GoogleLLM(BaseLLM):
|
||||
if "text" in item:
|
||||
parts.append(types.Part.from_text(text=item["text"]))
|
||||
elif "function_call" in item:
|
||||
# Remove null values from args to avoid API errors
|
||||
cleaned_args = self._remove_null_values(
|
||||
item["function_call"]["args"]
|
||||
)
|
||||
parts.append(
|
||||
types.Part.from_function_call(
|
||||
name=item["function_call"]["name"],
|
||||
args=item["function_call"]["args"],
|
||||
args=cleaned_args,
|
||||
)
|
||||
)
|
||||
elif "function_response" in item:
|
||||
@@ -386,7 +390,7 @@ class GoogleLLM(BaseLLM):
|
||||
elif hasattr(chunk, "text"):
|
||||
yield chunk.text
|
||||
finally:
|
||||
if hasattr(response, 'close'):
|
||||
if hasattr(response, "close"):
|
||||
response.close()
|
||||
|
||||
def _supports_tools(self):
|
||||
|
||||
@@ -44,14 +44,15 @@ class OpenAILLM(BaseLLM):
|
||||
{"role": role, "content": item["text"]}
|
||||
)
|
||||
elif "function_call" in item:
|
||||
cleaned_args = self._remove_null_values(
|
||||
item["function_call"]["args"]
|
||||
)
|
||||
tool_call = {
|
||||
"id": item["function_call"]["call_id"],
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": item["function_call"]["name"],
|
||||
"arguments": json.dumps(
|
||||
item["function_call"]["args"]
|
||||
),
|
||||
"arguments": json.dumps(cleaned_args),
|
||||
},
|
||||
}
|
||||
cleaned_messages.append(
|
||||
@@ -181,7 +182,7 @@ class OpenAILLM(BaseLLM):
|
||||
elif len(line.choices) > 0:
|
||||
yield line.choices[0]
|
||||
finally:
|
||||
if hasattr(response, 'close'):
|
||||
if hasattr(response, "close"):
|
||||
response.close()
|
||||
|
||||
def _supports_tools(self):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import logging
|
||||
from typing import List, Any
|
||||
from retry import retry
|
||||
from tqdm import tqdm
|
||||
from application.core.settings import settings
|
||||
@@ -22,13 +23,16 @@ def sanitize_content(content: str) -> str:
|
||||
|
||||
|
||||
@retry(tries=10, delay=60)
|
||||
def add_text_to_store_with_retry(store, doc, source_id):
|
||||
"""
|
||||
Add a document's text and metadata to the vector store with retry logic.
|
||||
def add_text_to_store_with_retry(store: Any, doc: Any, source_id: str) -> None:
|
||||
"""Add a document's text and metadata to the vector store with retry logic.
|
||||
|
||||
Args:
|
||||
store: The vector store object.
|
||||
doc: The document to be added.
|
||||
source_id: Unique identifier for the source.
|
||||
|
||||
Raises:
|
||||
Exception: If document addition fails after all retry attempts.
|
||||
"""
|
||||
try:
|
||||
# Sanitize content to remove NUL characters that cause ingestion failures
|
||||
@@ -41,18 +45,21 @@ def add_text_to_store_with_retry(store, doc, source_id):
|
||||
raise
|
||||
|
||||
|
||||
def embed_and_store_documents(docs, folder_name, source_id, task_status):
|
||||
"""
|
||||
Embeds documents and stores them in a vector store.
|
||||
def embed_and_store_documents(docs: List[Any], folder_name: str, source_id: str, task_status: Any) -> None:
|
||||
"""Embeds documents and stores them in a vector store.
|
||||
|
||||
Args:
|
||||
docs (list): List of documents to be embedded and stored.
|
||||
folder_name (str): Directory to save the vector store.
|
||||
source_id (str): Unique identifier for the source.
|
||||
docs: List of documents to be embedded and stored.
|
||||
folder_name: Directory to save the vector store.
|
||||
source_id: Unique identifier for the source.
|
||||
task_status: Task state manager for progress updates.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
OSError: If unable to create folder or save vector store.
|
||||
Exception: If vector store creation or document embedding fails.
|
||||
"""
|
||||
# Ensure the folder exists
|
||||
if not os.path.exists(folder_name):
|
||||
@@ -95,10 +102,21 @@ def embed_and_store_documents(docs, folder_name, source_id, task_status):
|
||||
except Exception as e:
|
||||
logging.error(f"Error embedding document {idx}: {e}", exc_info=True)
|
||||
logging.info(f"Saving progress at document {idx} out of {total_docs}")
|
||||
store.save_local(folder_name)
|
||||
try:
|
||||
store.save_local(folder_name)
|
||||
logging.info("Progress saved successfully")
|
||||
except Exception as save_error:
|
||||
logging.error(f"CRITICAL: Failed to save progress: {save_error}", exc_info=True)
|
||||
# Continue without breaking to attempt final save
|
||||
break
|
||||
|
||||
# Save the vector store
|
||||
if settings.VECTOR_STORE == "faiss":
|
||||
store.save_local(folder_name)
|
||||
logging.info("Vector store saved successfully.")
|
||||
try:
|
||||
store.save_local(folder_name)
|
||||
logging.info("Vector store saved successfully.")
|
||||
except Exception as e:
|
||||
logging.error(f"CRITICAL: Failed to save final vector store: {e}", exc_info=True)
|
||||
raise OSError(f"Unable to save vector store to {folder_name}: {e}") from e
|
||||
else:
|
||||
logging.info("Vector store saved successfully.")
|
||||
|
||||
@@ -30,7 +30,7 @@ jsonpatch==1.33
|
||||
jsonpointer==3.0.0
|
||||
kombu==5.4.2
|
||||
langchain==0.3.20
|
||||
langchain-community==0.4.1
|
||||
langchain-community==0.3.19
|
||||
langchain-core==0.3.59
|
||||
langchain-openai==0.3.16
|
||||
langchain-text-splitters==0.3.8
|
||||
|
||||
@@ -8,7 +8,3 @@ class BaseRetriever(ABC):
|
||||
@abstractmethod
|
||||
def search(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_params(self):
|
||||
pass
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
from application.core.settings import settings
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from application.retriever.base import BaseRetriever
|
||||
|
||||
from application.utils import num_tokens_from_string
|
||||
from application.vectorstore.vector_creator import VectorCreator
|
||||
|
||||
|
||||
@@ -15,14 +15,13 @@ class ClassicRAG(BaseRetriever):
|
||||
chat_history=None,
|
||||
prompt="",
|
||||
chunks=2,
|
||||
token_limit=150,
|
||||
doc_token_limit=50000,
|
||||
gpt_model="docsgpt",
|
||||
user_api_key=None,
|
||||
llm_name=settings.LLM_PROVIDER,
|
||||
api_key=settings.API_KEY,
|
||||
decoded_token=None,
|
||||
):
|
||||
"""Initialize ClassicRAG retriever with vectorstore sources and LLM configuration"""
|
||||
self.original_question = source.get("question", "")
|
||||
self.chat_history = chat_history if chat_history is not None else []
|
||||
self.prompt = prompt
|
||||
@@ -42,16 +41,7 @@ class ClassicRAG(BaseRetriever):
|
||||
f"sources={'active_docs' in source and source['active_docs'] is not None}"
|
||||
)
|
||||
self.gpt_model = gpt_model
|
||||
self.token_limit = (
|
||||
token_limit
|
||||
if token_limit
|
||||
< settings.LLM_TOKEN_LIMITS.get(
|
||||
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
||||
)
|
||||
else settings.LLM_TOKEN_LIMITS.get(
|
||||
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
||||
)
|
||||
)
|
||||
self.doc_token_limit = doc_token_limit
|
||||
self.user_api_key = user_api_key
|
||||
self.llm_name = llm_name
|
||||
self.api_key = api_key
|
||||
@@ -118,21 +108,17 @@ class ClassicRAG(BaseRetriever):
|
||||
return self.original_question
|
||||
|
||||
def _get_data(self):
|
||||
"""Retrieve relevant documents from configured vectorstores"""
|
||||
if self.chunks == 0 or not self.vectorstores:
|
||||
logging.info(
|
||||
f"ClassicRAG._get_data: Skipping retrieval - chunks={self.chunks}, "
|
||||
f"vectorstores_count={len(self.vectorstores) if self.vectorstores else 0}"
|
||||
)
|
||||
return []
|
||||
|
||||
all_docs = []
|
||||
chunks_per_source = max(1, self.chunks // len(self.vectorstores))
|
||||
|
||||
logging.info(
|
||||
f"ClassicRAG._get_data: Starting retrieval with chunks={self.chunks}, "
|
||||
f"vectorstores={self.vectorstores}, chunks_per_source={chunks_per_source}, "
|
||||
f"query='{self.question[:50]}...'"
|
||||
)
|
||||
token_budget = max(int(self.doc_token_limit * 0.9), 100)
|
||||
cumulative_tokens = 0
|
||||
|
||||
for vectorstore_id in self.vectorstores:
|
||||
if vectorstore_id:
|
||||
@@ -140,15 +126,21 @@ class ClassicRAG(BaseRetriever):
|
||||
docsearch = VectorCreator.create_vectorstore(
|
||||
settings.VECTOR_STORE, vectorstore_id, settings.EMBEDDINGS_KEY
|
||||
)
|
||||
docs_temp = docsearch.search(self.question, k=chunks_per_source)
|
||||
docs_temp = docsearch.search(
|
||||
self.question, k=max(chunks_per_source * 2, 20)
|
||||
)
|
||||
|
||||
for doc in docs_temp:
|
||||
if cumulative_tokens >= token_budget:
|
||||
break
|
||||
|
||||
if hasattr(doc, "page_content") and hasattr(doc, "metadata"):
|
||||
page_content = doc.page_content
|
||||
metadata = doc.metadata
|
||||
else:
|
||||
page_content = doc.get("text", doc.get("page_content", ""))
|
||||
metadata = doc.get("metadata", {})
|
||||
|
||||
title = metadata.get(
|
||||
"title", metadata.get("post_title", page_content)
|
||||
)
|
||||
@@ -168,23 +160,35 @@ class ClassicRAG(BaseRetriever):
|
||||
if not filename:
|
||||
filename = title
|
||||
source_path = metadata.get("source") or vectorstore_id
|
||||
all_docs.append(
|
||||
{
|
||||
"title": title,
|
||||
"text": page_content,
|
||||
"source": source_path,
|
||||
"filename": filename,
|
||||
}
|
||||
)
|
||||
|
||||
doc_text_with_header = f"{filename}\n{page_content}"
|
||||
doc_tokens = num_tokens_from_string(doc_text_with_header)
|
||||
|
||||
if cumulative_tokens + doc_tokens < token_budget:
|
||||
all_docs.append(
|
||||
{
|
||||
"title": title,
|
||||
"text": page_content,
|
||||
"source": source_path,
|
||||
"filename": filename,
|
||||
}
|
||||
)
|
||||
cumulative_tokens += doc_tokens
|
||||
|
||||
if cumulative_tokens >= token_budget:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Error searching vectorstore {vectorstore_id}: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
continue
|
||||
|
||||
logging.info(
|
||||
f"ClassicRAG._get_data: Retrieval complete - retrieved {len(all_docs)} documents "
|
||||
f"(requested chunks={self.chunks}, chunks_per_source={chunks_per_source})"
|
||||
f"(requested chunks={self.chunks}, chunks_per_source={chunks_per_source}, "
|
||||
f"cumulative_tokens={cumulative_tokens}/{token_budget})"
|
||||
)
|
||||
return all_docs
|
||||
|
||||
@@ -194,15 +198,3 @@ class ClassicRAG(BaseRetriever):
|
||||
self.original_question = query
|
||||
self.question = self._rephrase_query()
|
||||
return self._get_data()
|
||||
|
||||
def get_params(self):
|
||||
"""Return current retriever configuration parameters"""
|
||||
return {
|
||||
"question": self.original_question,
|
||||
"rephrased_question": self.question,
|
||||
"sources": self.vectorstores,
|
||||
"chunks": self.chunks,
|
||||
"token_limit": self.token_limit,
|
||||
"gpt_model": self.gpt_model,
|
||||
"user_api_key": self.user_api_key,
|
||||
}
|
||||
|
||||
0
application/templates/__init__.py
Normal file
0
application/templates/__init__.py
Normal file
190
application/templates/namespaces.py
Normal file
190
application/templates/namespaces.py
Normal file
@@ -0,0 +1,190 @@
|
||||
import logging
|
||||
import uuid
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NamespaceBuilder(ABC):
|
||||
"""Base class for building template context namespaces"""
|
||||
|
||||
@abstractmethod
|
||||
def build(self, **kwargs) -> Dict[str, Any]:
|
||||
"""Build namespace context dictionary"""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def namespace_name(self) -> str:
|
||||
"""Name of this namespace for template access"""
|
||||
pass
|
||||
|
||||
|
||||
class SystemNamespace(NamespaceBuilder):
|
||||
"""System metadata namespace: {{ system.* }}"""
|
||||
|
||||
@property
|
||||
def namespace_name(self) -> str:
|
||||
return "system"
|
||||
|
||||
def build(
|
||||
self, request_id: Optional[str] = None, user_id: Optional[str] = None, **kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Build system context with metadata.
|
||||
|
||||
Args:
|
||||
request_id: Unique request identifier
|
||||
user_id: Current user identifier
|
||||
|
||||
Returns:
|
||||
Dictionary with system variables
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
return {
|
||||
"date": now.strftime("%Y-%m-%d"),
|
||||
"time": now.strftime("%H:%M:%S"),
|
||||
"timestamp": now.isoformat(),
|
||||
"request_id": request_id or str(uuid.uuid4()),
|
||||
"user_id": user_id,
|
||||
}
|
||||
|
||||
|
||||
class PassthroughNamespace(NamespaceBuilder):
|
||||
"""Request parameters namespace: {{ passthrough.* }}"""
|
||||
|
||||
@property
|
||||
def namespace_name(self) -> str:
|
||||
return "passthrough"
|
||||
|
||||
def build(
|
||||
self, passthrough_data: Optional[Dict[str, Any]] = None, **kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Build passthrough context from request parameters.
|
||||
|
||||
Args:
|
||||
passthrough_data: Dictionary of parameters from web request
|
||||
|
||||
Returns:
|
||||
Dictionary with passthrough variables
|
||||
"""
|
||||
if not passthrough_data:
|
||||
return {}
|
||||
safe_data = {}
|
||||
for key, value in passthrough_data.items():
|
||||
if isinstance(value, (str, int, float, bool, type(None))):
|
||||
safe_data[key] = value
|
||||
else:
|
||||
logger.warning(
|
||||
f"Skipping non-serializable passthrough value for key '{key}': {type(value)}"
|
||||
)
|
||||
return safe_data
|
||||
|
||||
|
||||
class SourceNamespace(NamespaceBuilder):
|
||||
"""RAG source documents namespace: {{ source.* }}"""
|
||||
|
||||
@property
|
||||
def namespace_name(self) -> str:
|
||||
return "source"
|
||||
|
||||
def build(
|
||||
self, docs: Optional[list] = None, docs_together: Optional[str] = None, **kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Build source context from RAG retrieval results.
|
||||
|
||||
Args:
|
||||
docs: List of retrieved documents
|
||||
docs_together: Concatenated document content (for backward compatibility)
|
||||
|
||||
Returns:
|
||||
Dictionary with source variables
|
||||
"""
|
||||
context = {}
|
||||
|
||||
if docs:
|
||||
context["documents"] = docs
|
||||
context["count"] = len(docs)
|
||||
if docs_together:
|
||||
context["docs_together"] = docs_together # Add docs_together for custom templates
|
||||
context["content"] = docs_together
|
||||
context["summaries"] = docs_together
|
||||
return context
|
||||
|
||||
|
||||
class ToolsNamespace(NamespaceBuilder):
|
||||
"""Pre-executed tools namespace: {{ tools.* }}"""
|
||||
|
||||
@property
|
||||
def namespace_name(self) -> str:
|
||||
return "tools"
|
||||
|
||||
def build(
|
||||
self, tools_data: Optional[Dict[str, Any]] = None, **kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Build tools context with pre-executed tool results.
|
||||
|
||||
Args:
|
||||
tools_data: Dictionary of pre-fetched tool results organized by tool name
|
||||
e.g., {"memory": {"notes": "content", "tasks": "list"}}
|
||||
|
||||
Returns:
|
||||
Dictionary with tool results organized by tool name
|
||||
"""
|
||||
if not tools_data:
|
||||
return {}
|
||||
|
||||
safe_data = {}
|
||||
for tool_name, tool_result in tools_data.items():
|
||||
if isinstance(tool_result, (str, dict, list, int, float, bool, type(None))):
|
||||
safe_data[tool_name] = tool_result
|
||||
else:
|
||||
logger.warning(
|
||||
f"Skipping non-serializable tool result for '{tool_name}': {type(tool_result)}"
|
||||
)
|
||||
return safe_data
|
||||
|
||||
|
||||
class NamespaceManager:
|
||||
"""Manages all namespace builders and context assembly"""
|
||||
|
||||
def __init__(self):
|
||||
self._builders = {
|
||||
"system": SystemNamespace(),
|
||||
"passthrough": PassthroughNamespace(),
|
||||
"source": SourceNamespace(),
|
||||
"tools": ToolsNamespace(),
|
||||
}
|
||||
|
||||
def build_context(self, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Build complete template context from all namespaces.
|
||||
|
||||
Args:
|
||||
**kwargs: Parameters to pass to namespace builders
|
||||
|
||||
Returns:
|
||||
Complete context dictionary for template rendering
|
||||
"""
|
||||
context = {}
|
||||
|
||||
for namespace_name, builder in self._builders.items():
|
||||
try:
|
||||
namespace_context = builder.build(**kwargs)
|
||||
# Always include namespace, even if empty, to prevent undefined errors
|
||||
context[namespace_name] = namespace_context if namespace_context else {}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to build {namespace_name} namespace: {str(e)}")
|
||||
# Include empty namespace on error to prevent template failures
|
||||
context[namespace_name] = {}
|
||||
return context
|
||||
|
||||
def get_builder(self, namespace_name: str) -> Optional[NamespaceBuilder]:
|
||||
"""Get specific namespace builder"""
|
||||
return self._builders.get(namespace_name)
|
||||
161
application/templates/template_engine.py
Normal file
161
application/templates/template_engine.py
Normal file
@@ -0,0 +1,161 @@
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
|
||||
from jinja2 import (
|
||||
ChainableUndefined,
|
||||
Environment,
|
||||
nodes,
|
||||
select_autoescape,
|
||||
TemplateSyntaxError,
|
||||
)
|
||||
from jinja2.exceptions import UndefinedError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TemplateRenderError(Exception):
|
||||
"""Raised when template rendering fails"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TemplateEngine:
|
||||
"""Jinja2-based template engine for dynamic prompt rendering"""
|
||||
|
||||
def __init__(self):
|
||||
self._env = Environment(
|
||||
undefined=ChainableUndefined,
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
autoescape=select_autoescape(default_for_string=True, default=True),
|
||||
)
|
||||
|
||||
def render(self, template_content: str, context: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Render template with provided context.
|
||||
|
||||
Args:
|
||||
template_content: Raw template string with Jinja2 syntax
|
||||
context: Dictionary of variables to inject into template
|
||||
|
||||
Returns:
|
||||
Rendered template string
|
||||
|
||||
Raises:
|
||||
TemplateRenderError: If template syntax is invalid or variables undefined
|
||||
"""
|
||||
if not template_content:
|
||||
return ""
|
||||
try:
|
||||
template = self._env.from_string(template_content)
|
||||
return template.render(**context)
|
||||
except TemplateSyntaxError as e:
|
||||
error_msg = f"Template syntax error at line {e.lineno}: {e.message}"
|
||||
logger.error(error_msg)
|
||||
raise TemplateRenderError(error_msg) from e
|
||||
except UndefinedError as e:
|
||||
error_msg = f"Undefined variable in template: {e.message}"
|
||||
logger.error(error_msg)
|
||||
raise TemplateRenderError(error_msg) from e
|
||||
except Exception as e:
|
||||
error_msg = f"Template rendering failed: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
raise TemplateRenderError(error_msg) from e
|
||||
|
||||
def validate_template(self, template_content: str) -> bool:
|
||||
"""
|
||||
Validate template syntax without rendering.
|
||||
|
||||
Args:
|
||||
template_content: Template string to validate
|
||||
|
||||
Returns:
|
||||
True if template is syntactically valid
|
||||
"""
|
||||
if not template_content:
|
||||
return True
|
||||
try:
|
||||
self._env.from_string(template_content)
|
||||
return True
|
||||
except TemplateSyntaxError as e:
|
||||
logger.debug(f"Template syntax invalid at line {e.lineno}: {e.message}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.debug(f"Template validation error: {type(e).__name__}: {str(e)}")
|
||||
return False
|
||||
|
||||
def extract_variables(self, template_content: str) -> Set[str]:
|
||||
"""
|
||||
Extract all variable names from template.
|
||||
|
||||
Args:
|
||||
template_content: Template string to analyze
|
||||
|
||||
Returns:
|
||||
Set of variable names found in template
|
||||
"""
|
||||
if not template_content:
|
||||
return set()
|
||||
try:
|
||||
ast = self._env.parse(template_content)
|
||||
return set(self._env.get_template_module(ast).make_module().keys())
|
||||
except TemplateSyntaxError as e:
|
||||
logger.debug(f"Cannot extract variables - syntax error at line {e.lineno}")
|
||||
return set()
|
||||
except Exception as e:
|
||||
logger.debug(f"Cannot extract variables: {type(e).__name__}")
|
||||
return set()
|
||||
|
||||
def extract_tool_usages(
|
||||
self, template_content: str
|
||||
) -> Dict[str, Set[Optional[str]]]:
|
||||
"""Extract tool and action references from a template"""
|
||||
if not template_content:
|
||||
return {}
|
||||
try:
|
||||
ast = self._env.parse(template_content)
|
||||
except TemplateSyntaxError as e:
|
||||
logger.debug(f"extract_tool_usages - syntax error at line {e.lineno}")
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.debug(f"extract_tool_usages - parse error: {type(e).__name__}")
|
||||
return {}
|
||||
|
||||
usages: Dict[str, Set[Optional[str]]] = {}
|
||||
|
||||
def record(path: List[str]) -> None:
|
||||
if not path:
|
||||
return
|
||||
tool_name = path[0]
|
||||
action_name = path[1] if len(path) > 1 else None
|
||||
if not tool_name:
|
||||
return
|
||||
tool_entry = usages.setdefault(tool_name, set())
|
||||
tool_entry.add(action_name)
|
||||
|
||||
for node in ast.find_all(nodes.Getattr):
|
||||
path = []
|
||||
current = node
|
||||
while isinstance(current, nodes.Getattr):
|
||||
path.append(current.attr)
|
||||
current = current.node
|
||||
if isinstance(current, nodes.Name) and current.name == "tools":
|
||||
path.reverse()
|
||||
record(path)
|
||||
|
||||
for node in ast.find_all(nodes.Getitem):
|
||||
path = []
|
||||
current = node
|
||||
while isinstance(current, nodes.Getitem):
|
||||
key = current.arg
|
||||
if isinstance(key, nodes.Const) and isinstance(key.value, str):
|
||||
path.append(key.value)
|
||||
else:
|
||||
path = []
|
||||
break
|
||||
current = current.node
|
||||
if path and isinstance(current, nodes.Name) and current.name == "tools":
|
||||
path.reverse()
|
||||
record(path)
|
||||
|
||||
return usages
|
||||
@@ -74,6 +74,17 @@ def count_tokens_docs(docs):
|
||||
return tokens
|
||||
|
||||
|
||||
def calculate_doc_token_budget(
|
||||
gpt_model: str = "gpt-4o", history_token_limit: int = 2000
|
||||
) -> int:
|
||||
total_context = settings.LLM_TOKEN_LIMITS.get(
|
||||
gpt_model, settings.DEFAULT_LLM_TOKEN_LIMIT
|
||||
)
|
||||
reserved = sum(settings.RESERVED_TOKENS.values())
|
||||
doc_budget = total_context - history_token_limit - reserved
|
||||
return max(doc_budget, 1000)
|
||||
|
||||
|
||||
def get_missing_fields(data, required_fields):
|
||||
"""Check for missing required fields. Returns list of missing field names."""
|
||||
return [field for field in required_fields if field not in data]
|
||||
@@ -141,8 +152,8 @@ def limit_chat_history(history, max_token_limit=None, gpt_model="docsgpt"):
|
||||
max_token_limit
|
||||
if max_token_limit
|
||||
and max_token_limit
|
||||
< settings.LLM_TOKEN_LIMITS.get(gpt_model, settings.DEFAULT_MAX_HISTORY)
|
||||
else settings.LLM_TOKEN_LIMITS.get(gpt_model, settings.DEFAULT_MAX_HISTORY)
|
||||
< settings.LLM_TOKEN_LIMITS.get(gpt_model, settings.DEFAULT_LLM_TOKEN_LIMIT)
|
||||
else settings.LLM_TOKEN_LIMITS.get(gpt_model, settings.DEFAULT_LLM_TOKEN_LIMIT)
|
||||
)
|
||||
|
||||
if not history:
|
||||
@@ -187,3 +198,44 @@ def generate_image_url(image_path):
|
||||
else:
|
||||
base_url = getattr(settings, "API_URL", "http://localhost:7091")
|
||||
return f"{base_url}/api/images/{image_path}"
|
||||
|
||||
|
||||
def clean_text_for_tts(text: str) -> str:
|
||||
"""
|
||||
clean text for Text-to-Speech processing.
|
||||
"""
|
||||
# Handle code blocks and links
|
||||
text = re.sub(r'```mermaid[\s\S]*?```', ' flowchart, ', text) ## ```mermaid...```
|
||||
text = re.sub(r'```[\s\S]*?```', ' code block, ', text) ## ```code```
|
||||
text = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', text) ## [text](url)
|
||||
text = re.sub(r'!\[([^\]]*)\]\([^\)]+\)', '', text) ## 
|
||||
|
||||
# Remove markdown formatting
|
||||
text = re.sub(r'`([^`]+)`', r'\1', text) ## `code`
|
||||
text = re.sub(r'\{([^}]*)\}', r' \1 ', text) ## {text}
|
||||
text = re.sub(r'[{}]', ' ', text) ## unmatched {}
|
||||
text = re.sub(r'\[([^\]]+)\]', r' \1 ', text) ## [text]
|
||||
text = re.sub(r'[\[\]]', ' ', text) ## unmatched []
|
||||
text = re.sub(r'(\*\*|__)(.*?)\1', r'\2', text) ## **bold** __bold__
|
||||
text = re.sub(r'(\*|_)(.*?)\1', r'\2', text) ## *italic* _italic_
|
||||
text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE) ## # headers
|
||||
text = re.sub(r'^>\s+', '', text, flags=re.MULTILINE) ## > blockquotes
|
||||
text = re.sub(r'^[\s]*[-\*\+]\s+', '', text, flags=re.MULTILINE) ## - * + lists
|
||||
text = re.sub(r'^[\s]*\d+\.\s+', '', text, flags=re.MULTILINE) ## 1. numbered lists
|
||||
text = re.sub(r'^[\*\-_]{3,}\s*$', '', text, flags=re.MULTILINE) ## --- *** ___ rules
|
||||
text = re.sub(r'<[^>]*>', '', text) ## <html> tags
|
||||
|
||||
#Remove non-ASCII (emojis, special Unicode)
|
||||
text = re.sub(r'[^\x20-\x7E\n\r\t]', '', text)
|
||||
|
||||
#Replace special sequences
|
||||
text = re.sub(r'-->', ', ', text) ## -->
|
||||
text = re.sub(r'<--', ', ', text) ## <--
|
||||
text = re.sub(r'=>', ', ', text) ## =>
|
||||
text = re.sub(r'::', ' ', text) ## ::
|
||||
|
||||
#Normalize whitespace
|
||||
text = re.sub(r'\s+', ' ', text)
|
||||
text = text.strip()
|
||||
|
||||
return text
|
||||
|
||||
@@ -1,49 +1,453 @@
|
||||
---
|
||||
title: Customizing Prompts
|
||||
description: This guide will explain how to change prompts in DocsGPT and why it might be benefitial. Additionaly this article expains additional variables that can be used in prompts.
|
||||
title: Customizing Prompts
|
||||
description: This guide explains how to customize prompts in DocsGPT using the new template-based system with dynamic variable injection.
|
||||
---
|
||||
|
||||
import Image from 'next/image'
|
||||
|
||||
# Customizing the Main Prompt
|
||||
# Customizing Prompts in DocsGPT
|
||||
|
||||
Customizing the main prompt for DocsGPT gives you the ability to tailor the AI's responses to your specific requirements. By modifying the prompt text, you can achieve more accurate and relevant answers. Here's how you can do it:
|
||||
Customizing prompts for DocsGPT gives you powerful control over the AI's behavior and responses. With the new template-based system, you can inject dynamic context through organized namespaces, making prompts flexible and maintainable without hardcoding values.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Navigate to `SideBar -> Settings`.
|
||||
|
||||
|
||||
|
||||
|
||||
2.In Settings select the `Active Prompt` now you will be able to see various prompts style.x
|
||||
|
||||
|
||||
|
||||
|
||||
3.Click on the `edit icon` on the prompt of your choice and you will be able to see the current prompt for it,you can now customise the prompt as per your choice.
|
||||
2. In Settings, select the `Active Prompt` to see various prompt styles.
|
||||
3. Click on the `edit icon` on your chosen prompt to customize it.
|
||||
|
||||
### Video Demo
|
||||
<Image src="/prompts.gif" alt="prompts" width={800} height={500} />
|
||||
|
||||
---
|
||||
|
||||
## Template-Based Prompt System
|
||||
|
||||
## Example Prompt Modification
|
||||
DocsGPT now uses **Jinja2 templating** with four organized namespaces for dynamic variable injection:
|
||||
|
||||
### Available Namespaces
|
||||
|
||||
#### 1. **`system`** - System Metadata
|
||||
Access system-level information:
|
||||
|
||||
```jinja
|
||||
{{ system.date }} # Current date (YYYY-MM-DD)
|
||||
{{ system.time }} # Current time (HH:MM:SS)
|
||||
{{ system.timestamp }} # ISO 8601 timestamp
|
||||
{{ system.request_id }} # Unique request identifier
|
||||
{{ system.user_id }} # Current user ID
|
||||
```
|
||||
|
||||
#### 2. **`source`** - Retrieved Documents
|
||||
Access RAG (Retrieval-Augmented Generation) document context:
|
||||
|
||||
```jinja
|
||||
{{ source.content }} # Concatenated document content
|
||||
{{ source.summaries }} # Alias for content (backward compatible)
|
||||
{{ source.documents }} # List of document objects
|
||||
{{ source.count }} # Number of retrieved documents
|
||||
```
|
||||
|
||||
#### 3. **`passthrough`** - Request Parameters
|
||||
Access custom parameters passed in the API request:
|
||||
|
||||
```jinja
|
||||
{{ passthrough.company }} # Custom field from request
|
||||
{{ passthrough.user_name }} # User-provided data
|
||||
{{ passthrough.context }} # Any custom parameter
|
||||
```
|
||||
|
||||
To use passthrough data, send it in your API request:
|
||||
```json
|
||||
{
|
||||
"question": "What is the pricing?",
|
||||
"passthrough": {
|
||||
"company": "Acme Corp",
|
||||
"user_name": "Alice",
|
||||
"plan_type": "enterprise"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. **`tools`** - Pre-fetched Tool Data
|
||||
Access results from tools that run before the agent (like memory tool):
|
||||
|
||||
```jinja
|
||||
{{ tools.memory.root }} # Memory tool directory listing
|
||||
{{ tools.memory.available }} # Boolean: is memory available
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Prompts
|
||||
|
||||
### Basic Prompt with Documents
|
||||
```jinja
|
||||
You are a helpful AI assistant for DocsGPT.
|
||||
|
||||
Current date: {{ system.date }}
|
||||
|
||||
Use the following documents to answer the question:
|
||||
|
||||
{{ source.content }}
|
||||
|
||||
Provide accurate, helpful answers with code examples when relevant.
|
||||
```
|
||||
|
||||
### Advanced Prompt with All Namespaces
|
||||
```jinja
|
||||
You are an AI assistant for {{ passthrough.company }}.
|
||||
|
||||
**System Info:**
|
||||
- Date: {{ system.date }}
|
||||
- Request ID: {{ system.request_id }}
|
||||
|
||||
**User Context:**
|
||||
- User: {{ passthrough.user_name }}
|
||||
- Role: {{ passthrough.role }}
|
||||
|
||||
**Available Documents ({{ source.count }}):**
|
||||
{{ source.content }}
|
||||
|
||||
**Memory Context:**
|
||||
{% if tools.memory.available %}
|
||||
{{ tools.memory.root }}
|
||||
{% else %}
|
||||
No saved context available.
|
||||
{% endif %}
|
||||
|
||||
Please provide detailed, accurate answers based on the documents above.
|
||||
```
|
||||
|
||||
### Conditional Logic Example
|
||||
```jinja
|
||||
You are a DocsGPT assistant.
|
||||
|
||||
{% if source.count > 0 %}
|
||||
I found {{ source.count }} relevant document(s):
|
||||
|
||||
{{ source.content }}
|
||||
|
||||
Base your answer on these documents.
|
||||
{% else %}
|
||||
No documents were found. Please answer based on your general knowledge.
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Legacy Format (Still Supported)
|
||||
The old `{summaries}` format continues to work for backward compatibility:
|
||||
|
||||
**Original Prompt:**
|
||||
```markdown
|
||||
You are a DocsGPT, friendly and helpful AI assistant by Arc53 that provides help with documents. You give thorough answers with code examples if possible.
|
||||
Use the following pieces of context to help answer the users question. If it's not relevant to the question, provide friendly responses.
|
||||
You have access to chat history, and can use it to help answer the question.
|
||||
When using code examples, use the following format:
|
||||
You are a helpful assistant.
|
||||
|
||||
(code)
|
||||
Documents:
|
||||
{summaries}
|
||||
```
|
||||
|
||||
Note that `{summaries}` allows model to see and respond to your upploaded documents. If you don't want this functionality you can safely remove it from the customized prompt.
|
||||
This will automatically substitute `{summaries}` with document content.
|
||||
|
||||
Feel free to customize the prompt to align it with your specific use case or the kind of responses you want from the AI. For example, you can focus on specific document types, industries, or topics to get more targeted results.
|
||||
### New Template Format (Recommended)
|
||||
Migrate to the new template syntax for more flexibility:
|
||||
|
||||
```jinja
|
||||
You are a helpful assistant.
|
||||
|
||||
Documents:
|
||||
{{ source.content }}
|
||||
```
|
||||
|
||||
**Migration mapping:**
|
||||
- `{summaries}` → `{{ source.content }}` or `{{ source.summaries }}`
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. **Use Descriptive Context**
|
||||
```jinja
|
||||
**Retrieved Documents:**
|
||||
{{ source.content }}
|
||||
|
||||
**User Query Context:**
|
||||
- Company: {{ passthrough.company }}
|
||||
- Department: {{ passthrough.department }}
|
||||
```
|
||||
|
||||
### 2. **Handle Missing Data Gracefully**
|
||||
```jinja
|
||||
{% if passthrough.user_name %}
|
||||
Hello {{ passthrough.user_name }}!
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### 3. **Leverage Memory for Continuity**
|
||||
```jinja
|
||||
{% if tools.memory.available %}
|
||||
**Previous Context:**
|
||||
{{ tools.memory.root }}
|
||||
{% endif %}
|
||||
|
||||
**Current Question:**
|
||||
Please consider the above context when answering.
|
||||
```
|
||||
|
||||
### 4. **Add Clear Instructions**
|
||||
```jinja
|
||||
You are a technical support assistant.
|
||||
|
||||
**Guidelines:**
|
||||
1. Always reference the documents below
|
||||
2. Provide step-by-step instructions
|
||||
3. Include code examples when relevant
|
||||
|
||||
**Reference Documents:**
|
||||
{{ source.content }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Looping Over Documents
|
||||
```jinja
|
||||
{% for doc in source.documents %}
|
||||
**Source {{ loop.index }}:** {{ doc.filename }}
|
||||
{{ doc.text }}
|
||||
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### Date-Based Behavior
|
||||
```jinja
|
||||
{% if system.date > "2025-01-01" %}
|
||||
Note: This is information from 2025 or later.
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### Custom Formatting
|
||||
```jinja
|
||||
**Request Information**
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
• Request ID: {{ system.request_id }}
|
||||
• User: {{ passthrough.user_name | default("Guest") }}
|
||||
• Time: {{ system.time }}
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool Pre-Fetching
|
||||
|
||||
### Memory Tool Configuration
|
||||
Enable memory tool pre-fetching to inject saved context into prompts:
|
||||
|
||||
```python
|
||||
# In your tool configuration
|
||||
{
|
||||
"name": "memory",
|
||||
"config": {
|
||||
"pre_fetch_enabled": true # Default: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Control pre-fetching globally:
|
||||
```bash
|
||||
# .env file
|
||||
ENABLE_TOOL_PREFETCH=true
|
||||
```
|
||||
|
||||
Or per-request:
|
||||
```json
|
||||
{
|
||||
"question": "What are the requirements?",
|
||||
"disable_tool_prefetch": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Prompts
|
||||
|
||||
### View Rendered Prompts in Logs
|
||||
Set log level to `INFO` to see the final rendered prompt sent to the LLM:
|
||||
|
||||
```bash
|
||||
export LOG_LEVEL=INFO
|
||||
```
|
||||
|
||||
You'll see output like:
|
||||
```
|
||||
INFO - Rendered system prompt for agent (length: 1234 chars):
|
||||
================================================================================
|
||||
You are a helpful assistant for Acme Corp.
|
||||
|
||||
Current date: 2025-10-30
|
||||
Request ID: req_abc123
|
||||
|
||||
Documents:
|
||||
Technical documentation about...
|
||||
================================================================================
|
||||
```
|
||||
|
||||
### Template Validation
|
||||
Test your template syntax before saving:
|
||||
```python
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
is_valid = renderer.validate_template("Your prompt with {{ variables }}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### 1. Customer Support Bot
|
||||
```jinja
|
||||
You are a customer support assistant for {{ passthrough.company }}.
|
||||
|
||||
**Customer:** {{ passthrough.customer_name }}
|
||||
**Ticket ID:** {{ system.request_id }}
|
||||
**Date:** {{ system.date }}
|
||||
|
||||
**Knowledge Base:**
|
||||
{{ source.content }}
|
||||
|
||||
**Previous Interactions:**
|
||||
{{ tools.memory.root }}
|
||||
|
||||
Please provide helpful, friendly support based on the knowledge base above.
|
||||
```
|
||||
|
||||
### 2. Technical Documentation Assistant
|
||||
```jinja
|
||||
You are a technical documentation expert.
|
||||
|
||||
**Available Documentation ({{ source.count }} documents):**
|
||||
{{ source.content }}
|
||||
|
||||
**Requirements:**
|
||||
- Provide code examples in {{ passthrough.language }}
|
||||
- Focus on {{ passthrough.framework }} best practices
|
||||
- Include relevant links when possible
|
||||
```
|
||||
|
||||
### 3. Internal Knowledge Base
|
||||
```jinja
|
||||
You are an internal AI assistant for {{ passthrough.department }}.
|
||||
|
||||
**Employee:** {{ passthrough.employee_name }}
|
||||
**Access Level:** {{ passthrough.access_level }}
|
||||
|
||||
**Relevant Documents:**
|
||||
{{ source.content }}
|
||||
|
||||
Provide detailed answers appropriate for {{ passthrough.access_level }} access level.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template Syntax Reference
|
||||
|
||||
### Variables
|
||||
```jinja
|
||||
{{ variable_name }} # Output variable
|
||||
{{ namespace.field }} # Access nested field
|
||||
{{ variable | default("N/A") }} # Default value
|
||||
```
|
||||
|
||||
### Conditionals
|
||||
```jinja
|
||||
{% if condition %}
|
||||
Content
|
||||
{% elif other_condition %}
|
||||
Other content
|
||||
{% else %}
|
||||
Default content
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### Loops
|
||||
```jinja
|
||||
{% for item in list %}
|
||||
{{ item.field }}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### Comments
|
||||
```jinja
|
||||
{# This is a comment and won't appear in output #}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Input Sanitization**: Passthrough data is automatically sanitized to prevent injection attacks
|
||||
2. **Type Filtering**: Only primitive types (string, int, float, bool, None) are allowed in passthrough
|
||||
3. **Autoescaping**: Jinja2 autoescaping is enabled by default
|
||||
4. **Size Limits**: Consider the token budget when including large documents
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Variables Not Rendering
|
||||
**Solution:** Ensure you're using the correct namespace:
|
||||
```jinja
|
||||
❌ {{ company }}
|
||||
✅ {{ passthrough.company }}
|
||||
```
|
||||
|
||||
### Problem: Empty Output for Tool Data
|
||||
**Solution:** Check that tool pre-fetching is enabled and the tool is configured correctly.
|
||||
|
||||
### Problem: Syntax Errors
|
||||
**Solution:** Validate template syntax. Common issues:
|
||||
```jinja
|
||||
❌ {{ variable } # Missing closing brace
|
||||
❌ {% if x % # Missing closing %}
|
||||
✅ {{ variable }}
|
||||
✅ {% if x %}...{% endif %}
|
||||
```
|
||||
|
||||
### Problem: Legacy Prompts Not Working
|
||||
**Solution:** The system auto-detects template syntax. If your prompt uses `{summaries}`, it will work in legacy mode. To use new features, add `{{ }}` syntax.
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Render Prompt via API
|
||||
```python
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
rendered = renderer.render_prompt(
|
||||
prompt_content="Your template with {{ passthrough.name }}",
|
||||
user_id="user_123",
|
||||
request_id="req_456",
|
||||
passthrough_data={"name": "Alice"},
|
||||
docs_together="Document content here",
|
||||
tools_data={"memory": {"root": "Files: notes.txt"}}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
Customizing the main prompt for DocsGPT allows you to tailor the AI's responses to your unique requirements. Whether you need in-depth explanations, code examples, or specific insights, you can achieve it by modifying the main prompt. Remember to experiment and fine-tune your prompts to get the best results.
|
||||
The new template-based prompt system provides powerful flexibility while maintaining backward compatibility. By leveraging namespaces, you can create dynamic, context-aware prompts that adapt to your specific use case.
|
||||
|
||||
**Key Benefits:**
|
||||
- ✅ Dynamic variable injection
|
||||
- ✅ Organized namespaces
|
||||
- ✅ Backward compatible
|
||||
- ✅ Security built-in
|
||||
- ✅ Easy to debug
|
||||
|
||||
Start with simple templates and gradually add complexity as needed. Happy prompting! 🚀
|
||||
|
||||
@@ -57,7 +57,7 @@ The easiest way to launch DocsGPT is using the provided `setup.sh` script. This
|
||||
|
||||
* **4) Connect Cloud API Provider:** This option lets you connect DocsGPT to a commercial Cloud API provider such as OpenAI, Google (Vertex AI/Gemini), Anthropic (Claude), Groq, HuggingFace Inference API, or Azure OpenAI. You will need an API key from your chosen provider. Select this if you prefer to use a powerful cloud-based LLM.
|
||||
|
||||
* **5) Modify DocsGPT's source code and rebuild the Docker images locally. Instead of pulling prebuilt images from Docker Hub or using the hosted/public API, you build the entire backend and frontend from source, customizing how DocsGPT works internally, or run it in an environment without internet access.
|
||||
* **5) Modify DocsGPT's source code and rebuild the Docker images locally.** Instead of pulling prebuilt images from Docker Hub or using the hosted/public API, you build the entire backend and frontend from source, customizing how DocsGPT works internally, or run it in an environment without internet access.
|
||||
|
||||
After selecting an option and providing any required information (like API keys or model names), the script will configure your `.env` file and start DocsGPT using Docker Compose.
|
||||
|
||||
@@ -119,4 +119,4 @@ If you prefer a more manual approach, you can follow our [Docker Deployment docu
|
||||
|
||||
For more advanced customization of DocsGPT settings, such as configuring vector stores, embedding models, and other parameters, please refer to the [DocsGPT Settings documentation](/Deploying/DocsGPT-Settings). This guide explains how to modify the `.env` file or `settings.py` for deeper configuration.
|
||||
|
||||
Enjoy using DocsGPT!
|
||||
Enjoy using DocsGPT!
|
||||
|
||||
@@ -21,6 +21,9 @@ module.exports = {
|
||||
'react/prop-types': 'off',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'@typescript-eslint/no-unused-expressions': 'warn',
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
|
||||
5081
frontend/package-lock.json
generated
5081
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -19,21 +19,21 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@reduxjs/toolkit": "^2.10.1",
|
||||
"chart.js": "^4.4.4",
|
||||
"clsx": "^2.1.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"i18next": "^25.5.3",
|
||||
"i18next-browser-languagedetector": "^8.0.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mermaid": "^11.12.0",
|
||||
"mermaid": "^11.12.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.1.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-google-drive-picker": "^1.2.2",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-i18next": "^16.2.4",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.6.1",
|
||||
@@ -46,19 +46,17 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.10",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/mermaid": "^9.1.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.3",
|
||||
"@typescript-eslint/parser": "^8.46.3",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-config-standard-with-typescript": "^34.0.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-n": "^15.7.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-n": "^17.23.1",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-promise": "^6.6.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
@@ -66,10 +64,10 @@
|
||||
"lint-staged": "^15.3.0",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.13",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"prettier-plugin-tailwindcss": "^0.7.1",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite": "^7.2.0",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,7 +411,9 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
{recentAgents?.length > 0 ? (
|
||||
<div>
|
||||
<div className="mx-4 my-auto mt-2 flex h-6 items-center">
|
||||
<p className="mt-1 ml-4 text-sm font-semibold">Agents</p>
|
||||
<p className="mt-1 ml-4 text-sm font-semibold">
|
||||
{t('navigation.agents')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="agents-container">
|
||||
<div>
|
||||
@@ -565,7 +567,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
<div className="flex items-center gap-1 pr-4">
|
||||
<NavLink
|
||||
target="_blank"
|
||||
to={'https://discord.gg/WHJdfbQDR4'}
|
||||
to={'https://discord.gg/vN7YFfdMpj'}
|
||||
className={
|
||||
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function PageNotFound() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="dark:bg-raisin-black grid min-h-screen">
|
||||
<p className="text-jet dark:bg-outer-space mx-auto my-auto mt-20 flex w-full max-w-6xl flex-col place-items-center gap-6 rounded-3xl bg-gray-100 p-6 lg:p-10 xl:p-16 dark:text-gray-100">
|
||||
<h1>404</h1>
|
||||
<p>The page you are looking for does not exist.</p>
|
||||
<h1>{t('pageNotFound.title')}</h1>
|
||||
<p>{t('pageNotFound.message')}</p>
|
||||
<button className="pointer-cursor bg-blue-1000 hover:bg-blue-3000 mr-4 flex cursor-pointer items-center justify-center rounded-full px-4 py-2 text-white transition-colors duration-100">
|
||||
<Link to="/">Go Back Home</Link>
|
||||
<Link to="/">{t('pageNotFound.goHome')}</Link>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
@@ -11,6 +12,7 @@ import Logs from '../settings/Logs';
|
||||
import { Agent } from './types';
|
||||
|
||||
export default function AgentLogs() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { agentId } = useParams();
|
||||
const token = useSelector(selectToken);
|
||||
@@ -45,12 +47,12 @@ export default function AgentLogs() {
|
||||
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||
</button>
|
||||
<p className="text-eerie-black dark:text-bright-gray mt-px text-sm font-semibold">
|
||||
Back to all agents
|
||||
{t('agents.backToAll')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-5 flex w-full flex-wrap items-center justify-between gap-2 px-4">
|
||||
<h1 className="text-eerie-black m-0 text-[32px] font-bold md:text-[40px] dark:text-white">
|
||||
Agent Logs
|
||||
{t('agents.logs.title')}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-col gap-3 px-4">
|
||||
@@ -59,9 +61,10 @@ export default function AgentLogs() {
|
||||
<p className="text-[#28292E] dark:text-[#E0E0E0]">{agent.name}</p>
|
||||
<p className="text-xs text-[#28292E] dark:text-[#E0E0E0]/40">
|
||||
{agent.last_used_at
|
||||
? 'Last used at ' +
|
||||
? t('agents.logs.lastUsedAt') +
|
||||
' ' +
|
||||
new Date(agent.last_used_at).toLocaleString()
|
||||
: 'No usage history'}
|
||||
: t('agents.logs.noUsageHistory')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -79,7 +82,9 @@ export default function AgentLogs() {
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
agent && <Logs agentId={agent.id} tableHeader="Agent endpoint logs" />
|
||||
agent && (
|
||||
<Logs agentId={agent.id} tableHeader={t('agents.logs.tableHeader')} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import MessageInput from '../components/MessageInput';
|
||||
@@ -17,6 +18,7 @@ import { selectSelectedAgent } from '../preferences/preferenceSlice';
|
||||
import { AppDispatch } from '../store';
|
||||
|
||||
export default function AgentPreview() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
|
||||
const queries = useSelector(selectPreviewQueries);
|
||||
@@ -130,8 +132,7 @@ export default function AgentPreview() {
|
||||
/>
|
||||
</div>
|
||||
<p className="text-gray-4000 dark:text-sonic-silver w-full bg-transparent text-center text-xs md:inline">
|
||||
This is a preview of the agent. You can publish it to start using it
|
||||
in conversations.
|
||||
{t('agents.preview.testMessage')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
@@ -17,6 +18,7 @@ import { agentSectionsConfig } from './agents.config';
|
||||
import { Agent } from './types';
|
||||
|
||||
export default function AgentsList() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const token = useSelector(selectToken);
|
||||
const selectedAgent = useSelector(selectSelectedAgent);
|
||||
@@ -33,11 +35,10 @@ export default function AgentsList() {
|
||||
return (
|
||||
<div className="p-4 md:p-12">
|
||||
<h1 className="text-eerie-black mb-0 text-[32px] font-bold lg:text-[40px] dark:text-[#E0E0E0]">
|
||||
Agents
|
||||
{t('agents.title')}
|
||||
</h1>
|
||||
<p className="dark:text-gray-4000 mt-5 text-[15px] text-[#71717A]">
|
||||
Discover and create custom versions of DocsGPT that combine
|
||||
instructions, extra knowledge, and any combination of skills
|
||||
{t('agents.description')}
|
||||
</p>
|
||||
{agentSectionsConfig.map((sectionConfig) => (
|
||||
<AgentSection key={sectionConfig.id} config={sectionConfig} />
|
||||
@@ -51,6 +52,7 @@ function AgentSection({
|
||||
}: {
|
||||
config: (typeof agentSectionsConfig)[number];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const token = useSelector(selectToken);
|
||||
@@ -85,16 +87,18 @@ function AgentSection({
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-[18px] font-semibold text-[#18181B] dark:text-[#E0E0E0]">
|
||||
{config.title}
|
||||
{t(`agents.sections.${config.id}.title`)}
|
||||
</h2>
|
||||
<p className="text-[13px] text-[#71717A]">{config.description}</p>
|
||||
<p className="text-[13px] text-[#71717A]">
|
||||
{t(`agents.sections.${config.id}.description`)}
|
||||
</p>
|
||||
</div>
|
||||
{config.showNewAgentButton && (
|
||||
<button
|
||||
className="bg-purple-30 hover:bg-violets-are-blue rounded-full px-4 py-2 text-sm text-white"
|
||||
onClick={() => navigate('/agents/new')}
|
||||
>
|
||||
New Agent
|
||||
{t('agents.newAgent')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -117,13 +121,13 @@ function AgentSection({
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-72 w-full flex-col items-center justify-center gap-3 text-base text-[#18181B] dark:text-[#E0E0E0]">
|
||||
<p>{config.emptyStateDescription}</p>
|
||||
<p>{t(`agents.sections.${config.id}.emptyState`)}</p>
|
||||
{config.showNewAgentButton && (
|
||||
<button
|
||||
className="bg-purple-30 hover:bg-violets-are-blue ml-2 rounded-full px-4 py-2 text-sm text-white"
|
||||
onClick={() => navigate('/agents/new')}
|
||||
>
|
||||
New Agent
|
||||
{t('agents.newAgent')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
@@ -30,6 +31,7 @@ const embeddingsName =
|
||||
'huggingface_sentence-transformers/all-mpnet-base-v2';
|
||||
|
||||
export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const { agentId } = useParams();
|
||||
@@ -87,8 +89,8 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
|
||||
const modeConfig = {
|
||||
new: {
|
||||
heading: 'New Agent',
|
||||
buttonText: 'Publish',
|
||||
heading: t('agents.form.headings.new'),
|
||||
buttonText: t('agents.form.buttons.publish'),
|
||||
showDelete: false,
|
||||
showSaveDraft: true,
|
||||
showLogs: false,
|
||||
@@ -96,8 +98,8 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
trackChanges: false,
|
||||
},
|
||||
edit: {
|
||||
heading: 'Edit Agent',
|
||||
buttonText: 'Save',
|
||||
heading: t('agents.form.headings.edit'),
|
||||
buttonText: t('agents.form.buttons.save'),
|
||||
showDelete: true,
|
||||
showSaveDraft: false,
|
||||
showLogs: true,
|
||||
@@ -105,8 +107,8 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
trackChanges: true,
|
||||
},
|
||||
draft: {
|
||||
heading: 'New Agent (Draft)',
|
||||
buttonText: 'Publish',
|
||||
heading: t('agents.form.headings.draft'),
|
||||
buttonText: t('agents.form.buttons.publish'),
|
||||
showDelete: true,
|
||||
showSaveDraft: true,
|
||||
showLogs: false,
|
||||
@@ -116,8 +118,8 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
};
|
||||
const chunks = ['0', '2', '4', '6', '8', '10'];
|
||||
const agentTypes = [
|
||||
{ label: 'Classic', value: 'classic' },
|
||||
{ label: 'ReAct', value: 'react' },
|
||||
{ label: t('agents.form.agentTypes.classic'), value: 'classic' },
|
||||
{ label: t('agents.form.agentTypes.react'), value: 'react' },
|
||||
];
|
||||
|
||||
const isPublishable = () => {
|
||||
@@ -198,13 +200,19 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
|
||||
if (agent.limited_token_mode && agent.token_limit) {
|
||||
formData.append('limited_token_mode', 'True');
|
||||
formData.append('token_limit', JSON.stringify(agent.token_limit));
|
||||
} else formData.append('token_limit', '0');
|
||||
formData.append('token_limit', agent.token_limit.toString());
|
||||
} else {
|
||||
formData.append('limited_token_mode', 'False');
|
||||
formData.append('token_limit', '0');
|
||||
}
|
||||
|
||||
if (agent.limited_request_mode && agent.request_limit) {
|
||||
formData.append('limited_request_mode', 'True');
|
||||
formData.append('request_limit', JSON.stringify(agent.request_limit));
|
||||
} else formData.append('request_limit', '0');
|
||||
formData.append('request_limit', agent.request_limit.toString());
|
||||
} else {
|
||||
formData.append('limited_request_mode', 'False');
|
||||
formData.append('request_limit', '0');
|
||||
}
|
||||
|
||||
if (imageFile) formData.append('image', imageFile);
|
||||
|
||||
@@ -295,15 +303,22 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
formData.append('json_schema', JSON.stringify(agent.json_schema));
|
||||
}
|
||||
|
||||
// Always send the limited mode fields
|
||||
if (agent.limited_token_mode && agent.token_limit) {
|
||||
formData.append('limited_token_mode', 'True');
|
||||
formData.append('token_limit', JSON.stringify(agent.token_limit));
|
||||
} else formData.append('token_limit', '0');
|
||||
formData.append('token_limit', agent.token_limit.toString());
|
||||
} else {
|
||||
formData.append('limited_token_mode', 'False');
|
||||
formData.append('token_limit', '0');
|
||||
}
|
||||
|
||||
if (agent.limited_request_mode && agent.request_limit) {
|
||||
formData.append('limited_request_mode', 'True');
|
||||
formData.append('request_limit', JSON.stringify(agent.request_limit));
|
||||
} else formData.append('request_limit', '0');
|
||||
formData.append('request_limit', agent.request_limit.toString());
|
||||
} else {
|
||||
formData.append('limited_request_mode', 'False');
|
||||
formData.append('request_limit', '0');
|
||||
}
|
||||
|
||||
try {
|
||||
setPublishLoading(true);
|
||||
@@ -543,7 +558,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
|
||||
</button>
|
||||
<p className="text-eerie-black dark:text-bright-gray mt-px text-sm font-semibold">
|
||||
Back to all agents
|
||||
{t('agents.backToAll')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-5 flex w-full flex-wrap items-center justify-between gap-2 px-4">
|
||||
@@ -555,7 +570,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
className="text-purple-30 dark:text-light-gray mr-4 rounded-3xl py-2 text-sm font-medium dark:bg-transparent"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
{t('agents.form.buttons.cancel')}
|
||||
</button>
|
||||
{modeConfig[effectiveMode].showDelete && agent.id && (
|
||||
<button
|
||||
@@ -563,7 +578,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
onClick={() => setDeleteConfirmation('ACTIVE')}
|
||||
>
|
||||
<span className="block h-4 w-4 bg-[url('/src/assets/red-trash.svg')] bg-contain bg-center bg-no-repeat transition-all group-hover:bg-[url('/src/assets/white-trash.svg')]" />
|
||||
Delete
|
||||
{t('agents.form.buttons.delete')}
|
||||
</button>
|
||||
)}
|
||||
{modeConfig[effectiveMode].showSaveDraft && (
|
||||
@@ -578,7 +593,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
{draftLoading ? (
|
||||
<Spinner size="small" color="#976af3" />
|
||||
) : (
|
||||
'Save Draft'
|
||||
t('agents.form.buttons.saveDraft')
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
@@ -589,7 +604,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
onClick={() => navigate(`/agents/logs/${agent.id}`)}
|
||||
>
|
||||
<span className="block h-5 w-5 bg-[url('/src/assets/monitoring-purple.svg')] bg-contain bg-center bg-no-repeat transition-all group-hover:bg-[url('/src/assets/monitoring-white.svg')]" />
|
||||
Logs
|
||||
{t('agents.form.buttons.logs')}
|
||||
</button>
|
||||
)}
|
||||
{modeConfig[effectiveMode].showAccessDetails && (
|
||||
@@ -597,7 +612,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
className="hover:bg-vi</button>olets-are-blue border-violets-are-blue text-violets-are-blue hover:bg-violets-are-blue rounded-3xl border border-solid px-5 py-2 text-sm font-medium transition-colors hover:text-white"
|
||||
onClick={() => setAgentDetails('ACTIVE')}
|
||||
>
|
||||
Access Details
|
||||
{t('agents.form.buttons.accessDetails')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
@@ -618,17 +633,19 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
<div className="mt-3 flex w-full flex-1 grid-cols-5 flex-col gap-10 rounded-[30px] bg-[#F6F6F6] p-5 max-[1179px]:overflow-visible min-[1180px]:grid min-[1180px]:gap-5 min-[1180px]:overflow-hidden dark:bg-[#383838]">
|
||||
<div className="scrollbar-thin col-span-2 flex flex-col gap-5 max-[1179px]:overflow-visible min-[1180px]:max-h-full min-[1180px]:overflow-y-auto min-[1180px]:pr-3">
|
||||
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
|
||||
<h2 className="text-lg font-semibold">Meta</h2>
|
||||
<h2 className="text-lg font-semibold">
|
||||
{t('agents.form.sections.meta')}
|
||||
</h2>
|
||||
<input
|
||||
className="border-silver text-jet dark:bg-raisin-black dark:text-bright-gray dark:placeholder:text-silver mt-3 w-full rounded-3xl border bg-white px-5 py-3 text-sm outline-hidden placeholder:text-gray-400 dark:border-[#7E7E7E]"
|
||||
type="text"
|
||||
value={agent.name}
|
||||
placeholder="Agent name"
|
||||
placeholder={t('agents.form.placeholders.agentName')}
|
||||
onChange={(e) => setAgent({ ...agent, name: e.target.value })}
|
||||
/>
|
||||
<textarea
|
||||
className="border-silver text-jet dark:bg-raisin-black dark:text-bright-gray dark:placeholder:text-silver mt-3 h-32 w-full rounded-xl border bg-white px-5 py-4 text-sm outline-hidden placeholder:text-gray-400 dark:border-[#7E7E7E]"
|
||||
placeholder="Describe your agent"
|
||||
placeholder={t('agents.form.placeholders.describeAgent')}
|
||||
value={agent.description}
|
||||
onChange={(e) =>
|
||||
setAgent({ ...agent, description: e.target.value })
|
||||
@@ -641,9 +658,12 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
onUpload={handleUpload}
|
||||
onRemove={() => setImageFile(null)}
|
||||
uploadText={[
|
||||
{ text: 'Click to upload', colorClass: 'text-[#7D54D1]' },
|
||||
{
|
||||
text: ' or drag and drop',
|
||||
text: t('agents.form.upload.clickToUpload'),
|
||||
colorClass: 'text-[#7D54D1]',
|
||||
},
|
||||
{
|
||||
text: t('agents.form.upload.dragAndDrop'),
|
||||
colorClass: 'text-[#525252]',
|
||||
},
|
||||
]}
|
||||
@@ -651,7 +671,9 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
|
||||
<h2 className="text-lg font-semibold">Source</h2>
|
||||
<h2 className="text-lg font-semibold">
|
||||
{t('agents.form.sections.source')}
|
||||
</h2>
|
||||
<div className="mt-3">
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<button
|
||||
@@ -672,11 +694,13 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
source.name === id ||
|
||||
source.retriever === id,
|
||||
);
|
||||
return matchedDoc?.name || `External KB`;
|
||||
return (
|
||||
matchedDoc?.name || t('agents.form.externalKb')
|
||||
);
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ')
|
||||
: 'Select sources'}
|
||||
: t('agents.form.placeholders.selectSources')}
|
||||
</button>
|
||||
<MultiSelectPopup
|
||||
isOpen={isSourcePopupOpen}
|
||||
@@ -720,9 +744,13 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
setSelectedSourceIds(newSelectedIds);
|
||||
}
|
||||
}}
|
||||
title="Select Sources"
|
||||
searchPlaceholder="Search sources..."
|
||||
noOptionsMessage="No sources available"
|
||||
title={t('agents.form.sourcePopup.title')}
|
||||
searchPlaceholder={t(
|
||||
'agents.form.sourcePopup.searchPlaceholder',
|
||||
)}
|
||||
noOptionsMessage={t(
|
||||
'agents.form.sourcePopup.noOptionsMessage',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
@@ -737,7 +765,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
border="border"
|
||||
buttonClassName="bg-white dark:bg-[#222327] border-silver dark:border-[#7E7E7E]"
|
||||
optionsClassName="bg-white dark:bg-[#383838] border-silver dark:border-[#7E7E7E]"
|
||||
placeholder="Chunks per query"
|
||||
placeholder={t('agents.form.placeholders.chunksPerQuery')}
|
||||
placeholderClassName="text-gray-400 dark:text-silver"
|
||||
contentSize="text-sm"
|
||||
/>
|
||||
@@ -757,7 +785,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
setAgent({ ...agent, prompt_id: id })
|
||||
}
|
||||
setPrompts={setPrompts}
|
||||
title="Prompt"
|
||||
title={t('agents.form.sections.prompt')}
|
||||
titleClassName="text-lg font-semibold dark:text-[#E0E0E0]"
|
||||
showAddButton={false}
|
||||
dropdownProps={{
|
||||
@@ -777,12 +805,14 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
className="border-violets-are-blue text-violets-are-blue hover:bg-violets-are-blue w-20 shrink-0 basis-full rounded-3xl border-2 border-solid px-5 py-[11px] text-sm transition-colors hover:text-white sm:basis-auto"
|
||||
onClick={() => setAddPromptModal('ACTIVE')}
|
||||
>
|
||||
Add
|
||||
{t('agents.form.buttons.add')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
|
||||
<h2 className="text-lg font-semibold">Tools</h2>
|
||||
<h2 className="text-lg font-semibold">
|
||||
{t('agents.form.sections.tools')}
|
||||
</h2>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-1">
|
||||
<button
|
||||
ref={toolAnchorButtonRef}
|
||||
@@ -798,7 +828,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
.map((tool) => tool.display_name || tool.name)
|
||||
.filter(Boolean)
|
||||
.join(', ')
|
||||
: 'Select tools'}
|
||||
: t('agents.form.placeholders.selectTools')}
|
||||
</button>
|
||||
<MultiSelectPopup
|
||||
isOpen={isToolsPopupOpen}
|
||||
@@ -817,14 +847,18 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
})),
|
||||
)
|
||||
}
|
||||
title="Select Tools"
|
||||
searchPlaceholder="Search tools..."
|
||||
noOptionsMessage="No tools available"
|
||||
title={t('agents.form.toolsPopup.title')}
|
||||
searchPlaceholder={t(
|
||||
'agents.form.toolsPopup.searchPlaceholder',
|
||||
)}
|
||||
noOptionsMessage={t('agents.form.toolsPopup.noOptionsMessage')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dark:bg-raisin-black rounded-[30px] bg-white px-6 py-3 dark:text-[#E0E0E0]">
|
||||
<h2 className="text-lg font-semibold">Agent type</h2>
|
||||
<h2 className="text-lg font-semibold">
|
||||
{t('agents.form.sections.agentType')}
|
||||
</h2>
|
||||
<div className="mt-3">
|
||||
<Dropdown
|
||||
options={agentTypes}
|
||||
@@ -842,7 +876,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
border="border"
|
||||
buttonClassName="bg-white dark:bg-[#222327] border-silver dark:border-[#7E7E7E]"
|
||||
optionsClassName="bg-white dark:bg-[#383838] border-silver dark:border-[#7E7E7E]"
|
||||
placeholder="Select type"
|
||||
placeholder={t('agents.form.placeholders.selectType')}
|
||||
placeholderClassName="text-gray-400 dark:text-silver"
|
||||
contentSize="text-sm"
|
||||
/>
|
||||
@@ -856,7 +890,9 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
className="flex w-full items-center justify-between text-left focus:outline-none"
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Advanced</h2>
|
||||
<h2 className="text-lg font-semibold">
|
||||
{t('agents.form.sections.advanced')}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="ml-4 flex items-center">
|
||||
<svg
|
||||
@@ -879,9 +915,11 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
{isAdvancedSectionExpanded && (
|
||||
<div className="mt-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">JSON response schema</h2>
|
||||
<h2 className="text-sm font-medium">
|
||||
{t('agents.form.advanced.jsonSchema')}
|
||||
</h2>
|
||||
<p className="mt-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
Define a JSON schema to enforce structured output format
|
||||
{t('agents.form.advanced.jsonSchemaDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<textarea
|
||||
@@ -915,17 +953,19 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
}`}
|
||||
/>
|
||||
{jsonSchemaValid
|
||||
? 'Valid JSON'
|
||||
: 'Invalid JSON - fix to enable saving'}
|
||||
? t('agents.form.advanced.validJson')
|
||||
: t('agents.form.advanced.invalidJson')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">Token limiting</h2>
|
||||
<h2 className="text-sm font-medium">
|
||||
{t('agents.form.advanced.tokenLimiting')}
|
||||
</h2>
|
||||
<p className="mt-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
Limit daily total tokens that can be used by this agent
|
||||
{t('agents.form.advanced.tokenLimitingDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -965,7 +1005,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
})
|
||||
}
|
||||
disabled={!agent.limited_token_mode}
|
||||
placeholder="Enter token limit"
|
||||
placeholder={t('agents.form.placeholders.enterTokenLimit')}
|
||||
className={`border-silver text-jet dark:bg-raisin-black dark:text-bright-gray dark:placeholder:text-silver mt-2 w-full rounded-3xl border bg-white px-5 py-3 text-sm outline-hidden placeholder:text-gray-400 dark:border-[#7E7E7E] ${
|
||||
!agent.limited_token_mode
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
@@ -977,10 +1017,11 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">Request limiting</h2>
|
||||
<h2 className="text-sm font-medium">
|
||||
{t('agents.form.advanced.requestLimiting')}
|
||||
</h2>
|
||||
<p className="mt-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
Limit daily total requests that can be made to this
|
||||
agent
|
||||
{t('agents.form.advanced.requestLimitingDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -1020,7 +1061,9 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
})
|
||||
}
|
||||
disabled={!agent.limited_request_mode}
|
||||
placeholder="Enter request limit"
|
||||
placeholder={t(
|
||||
'agents.form.placeholders.enterRequestLimit',
|
||||
)}
|
||||
className={`border-silver text-jet dark:bg-raisin-black dark:text-bright-gray dark:placeholder:text-silver mt-2 w-full rounded-3xl border bg-white px-5 py-3 text-sm outline-hidden placeholder:text-gray-400 dark:border-[#7E7E7E] ${
|
||||
!agent.limited_request_mode
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
@@ -1033,22 +1076,24 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-3 flex flex-col gap-2 max-[1179px]:h-auto max-[1179px]:px-0 max-[1179px]:py-0 min-[1180px]:h-full min-[1180px]:py-2 dark:text-[#E0E0E0]">
|
||||
<h2 className="text-lg font-semibold">Preview</h2>
|
||||
<h2 className="text-lg font-semibold">
|
||||
{t('agents.form.sections.preview')}
|
||||
</h2>
|
||||
<div className="flex-1 max-[1179px]:overflow-visible min-[1180px]:min-h-0 min-[1180px]:overflow-hidden">
|
||||
<AgentPreviewArea />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
message="Are you sure you want to delete this agent?"
|
||||
message={t('agents.deleteConfirmation')}
|
||||
modalState={deleteConfirmation}
|
||||
setModalState={setDeleteConfirmation}
|
||||
submitLabel="Delete"
|
||||
submitLabel={t('agents.form.buttons.delete')}
|
||||
handleSubmit={() => {
|
||||
handleDelete(agent.id || '');
|
||||
setDeleteConfirmation('INACTIVE');
|
||||
}}
|
||||
cancelLabel="Cancel"
|
||||
cancelLabel={t('agents.form.buttons.cancel')}
|
||||
variant="danger"
|
||||
/>
|
||||
<AgentDetailsModal
|
||||
@@ -1071,6 +1116,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
}
|
||||
|
||||
function AgentPreviewArea() {
|
||||
const { t } = useTranslation();
|
||||
const selectedAgent = useSelector(selectSelectedAgent);
|
||||
return (
|
||||
<div className="dark:bg-raisin-black w-full rounded-[30px] border border-[#F6F6F6] bg-white max-[1179px]:h-[600px] min-[1180px]:h-full dark:border-[#7E7E7E]">
|
||||
@@ -1082,7 +1128,7 @@ function AgentPreviewArea() {
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-2">
|
||||
<span className="block h-12 w-12 bg-[url('/src/assets/science-spark.svg')] bg-contain bg-center bg-no-repeat transition-all dark:bg-[url('/src/assets/science-spark-dark.svg')]" />{' '}
|
||||
<p className="dark:text-gray-4000 text-xs text-[#18181B]">
|
||||
Published agents can be previewed here
|
||||
{t('agents.form.preview.publishedPreview')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -144,7 +144,7 @@ export default function SharedAgent() {
|
||||
className="mx-auto mb-6 h-32 w-32"
|
||||
/>
|
||||
<p className="dark:text-gray-4000 text-center text-lg text-[#71717A]">
|
||||
No agent found. Please ensure the agent is shared.
|
||||
{t('agents.shared.notFound')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
3
frontend/src/assets/book.svg
Normal file
3
frontend/src/assets/book.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.2857 14H2.57143C1.15179 14 0 12.8242 0 11.375V2.625C0 1.17578 1.15179 0 2.57143 0H10.7143C11.4241 0 12 0.587891 12 1.3125V9.1875C12 9.75898 11.6411 10.2457 11.1429 10.4262V12.25C11.617 12.25 12 12.641 12 13.125C12 13.609 11.617 14 11.1429 14H10.2857ZM2.57143 10.5C2.09732 10.5 1.71429 10.891 1.71429 11.375C1.71429 11.859 2.09732 12.25 2.57143 12.25H9.42857V10.5H2.57143ZM3.42857 4.15625C3.42857 4.51992 3.71518 4.8125 4.07143 4.8125H8.78571C9.14196 4.8125 9.42857 4.51992 9.42857 4.15625C9.42857 3.79258 9.14196 3.5 8.78571 3.5H4.07143C3.71518 3.5 3.42857 3.79258 3.42857 4.15625ZM4.07143 6.125C3.71518 6.125 3.42857 6.41758 3.42857 6.78125C3.42857 7.14492 3.71518 7.4375 4.07143 7.4375H8.78571C9.14196 7.4375 9.42857 7.14492 9.42857 6.78125C9.42857 6.41758 9.14196 6.125 8.78571 6.125H4.07143Z" fill="#6A4DF4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 930 B |
@@ -45,7 +45,7 @@ export default function ActionButtons({
|
||||
<div className={`flex items-center gap-2 sm:gap-4 ${className}`}>
|
||||
{showNewChat && (
|
||||
<button
|
||||
title="Open New Chat"
|
||||
title={t('actionButtons.openNewChat')}
|
||||
onClick={newChat}
|
||||
className="hover:bg-bright-gray flex items-center gap-1 rounded-full p-2 lg:hidden dark:hover:bg-[#28292E]"
|
||||
>
|
||||
@@ -62,7 +62,7 @@ export default function ActionButtons({
|
||||
{showShare && conversationId && (
|
||||
<>
|
||||
<button
|
||||
title="Share"
|
||||
title={t('actionButtons.share')}
|
||||
onClick={() => setShareModalState(true)}
|
||||
className="hover:bg-bright-gray rounded-full p-2 dark:hover:bg-[#28292E]"
|
||||
>
|
||||
|
||||
@@ -38,7 +38,7 @@ interface DirectoryStructure {
|
||||
[key: string]: FileNode;
|
||||
}
|
||||
|
||||
interface ConnectorTreeComponentProps {
|
||||
interface ConnectorTreeProps {
|
||||
docId: string;
|
||||
sourceName: string;
|
||||
onBackToDocuments: () => void;
|
||||
@@ -50,7 +50,7 @@ interface SearchResult {
|
||||
isFile: boolean;
|
||||
}
|
||||
|
||||
const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
const ConnectorTree: React.FC<ConnectorTreeProps> = ({
|
||||
docId,
|
||||
sourceName,
|
||||
onBackToDocuments,
|
||||
@@ -744,4 +744,4 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectorTreeComponent;
|
||||
export default ConnectorTree;
|
||||
@@ -60,7 +60,7 @@ function Dropdown<T extends DropdownOption>({
|
||||
}`}
|
||||
>
|
||||
{typeof selectedValue === 'string' ? (
|
||||
<span className="dark:text-bright-gray truncate">
|
||||
<span className={`dark:text-bright-gray truncate ${contentSize}`}>
|
||||
{selectedValue}
|
||||
</span>
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { formatBytes } from '../utils/stringUtils';
|
||||
import { formatDate } from '../utils/dateTimeUtils';
|
||||
import {
|
||||
@@ -66,6 +67,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [files, setFiles] = useState<CloudFile[]>([]);
|
||||
const [selectedFiles, setSelectedFiles] =
|
||||
useState<string[]>(initialSelectedFiles);
|
||||
@@ -417,7 +419,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
<div className="mb-3 max-w-md">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search files and folders..."
|
||||
placeholder={t('filePicker.searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
colorVariant="silver"
|
||||
@@ -431,7 +433,9 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
|
||||
{/* Selected Files Message */}
|
||||
<div className="pb-3 text-sm text-gray-600 dark:text-gray-400">
|
||||
{selectedFiles.length + selectedFolders.length} selected
|
||||
{t('filePicker.itemsSelected', {
|
||||
count: selectedFiles.length + selectedFolders.length,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -448,9 +452,15 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeader width="40px"></TableHeader>
|
||||
<TableHeader width="60%">Name</TableHeader>
|
||||
<TableHeader width="20%">Last Modified</TableHeader>
|
||||
<TableHeader width="20%">Size</TableHeader>
|
||||
<TableHeader width="60%">
|
||||
{t('filePicker.name')}
|
||||
</TableHeader>
|
||||
<TableHeader width="20%">
|
||||
{t('filePicker.lastModified')}
|
||||
</TableHeader>
|
||||
<TableHeader width="20%">
|
||||
{t('filePicker.size')}
|
||||
</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
||||
@@ -36,7 +36,7 @@ interface DirectoryStructure {
|
||||
[key: string]: FileNode;
|
||||
}
|
||||
|
||||
interface FileTreeComponentProps {
|
||||
interface FileTreeProps {
|
||||
docId: string;
|
||||
sourceName: string;
|
||||
onBackToDocuments: () => void;
|
||||
@@ -48,7 +48,7 @@ interface SearchResult {
|
||||
isFile: boolean;
|
||||
}
|
||||
|
||||
const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
const FileTree: React.FC<FileTreeProps> = ({
|
||||
docId,
|
||||
sourceName,
|
||||
onBackToDocuments,
|
||||
@@ -871,4 +871,4 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default FileTreeComponent;
|
||||
export default FileTree;
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
@@ -44,13 +45,14 @@ export const FileUpload = ({
|
||||
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',
|
||||
uploadText,
|
||||
dragActiveText,
|
||||
fileTypeText,
|
||||
sizeLimitText,
|
||||
disabled = false,
|
||||
validator,
|
||||
}: FileUploadProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
const [preview, setPreview] = useState<string | null>(null);
|
||||
const [currentFile, setCurrentFile] = useState<File | null>(null);
|
||||
@@ -71,7 +73,9 @@ export const FileUpload = ({
|
||||
if (file.size > maxSize) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `File exceeds ${maxSize / 1024 / 1024}MB limit`,
|
||||
error: t('components.fileUpload.fileSizeError', {
|
||||
size: maxSize / 1024 / 1024,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -178,7 +182,11 @@ export const FileUpload = ({
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return <p className="text-sm font-semibold">{uploadText}</p>;
|
||||
return (
|
||||
<p className="text-sm font-semibold">
|
||||
{uploadText || t('components.fileUpload.clickToUpload')}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultContent = (
|
||||
@@ -196,14 +204,17 @@ export const FileUpload = ({
|
||||
<div className="text-center">
|
||||
<div className="text-sm font-medium">
|
||||
{isDragActive ? (
|
||||
<p className="text-sm font-semibold">{dragActiveText}</p>
|
||||
<p className="text-sm font-semibold">
|
||||
{dragActiveText || t('components.fileUpload.dropFiles')}
|
||||
</p>
|
||||
) : (
|
||||
renderUploadText()
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-[#A3A3A3]">
|
||||
{fileTypeText} {maxSize / 1024 / 1024}
|
||||
{sizeLimitText}
|
||||
{fileTypeText || t('components.fileUpload.fileTypes')}{' '}
|
||||
{maxSize / 1024 / 1024}
|
||||
{sizeLimitText || t('components.fileUpload.sizeLimitUnit')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@ const Input = ({
|
||||
onChange,
|
||||
onPaste,
|
||||
onKeyDown,
|
||||
edgeRoundness = 'rounded-full',
|
||||
}: InputProps) => {
|
||||
const colorStyles = {
|
||||
silver: 'border-silver dark:border-silver/40',
|
||||
@@ -43,7 +44,7 @@ const Input = ({
|
||||
<div className={`relative ${className}`}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={`peer text-jet dark:text-bright-gray h-[42px] w-full rounded-full bg-transparent ${leftIcon ? 'pl-10' : 'px-3'} py-1 placeholder-transparent outline-hidden ${colorStyles[colorVariant]} ${borderStyles[borderVariant]} ${textSizeStyles[textSize]} [&:-webkit-autofill]:appearance-none [&:-webkit-autofill]:bg-transparent [&:-webkit-autofill_selected]:bg-transparent`}
|
||||
className={`peer text-jet dark:text-bright-gray h-[42px] w-full ${edgeRoundness} bg-transparent ${leftIcon ? 'pl-10' : 'px-3'} py-1 placeholder-transparent outline-hidden ${colorStyles[colorVariant]} ${borderStyles[borderVariant]} ${textSizeStyles[textSize]} [&:-webkit-autofill]:appearance-none [&:-webkit-autofill]:bg-transparent [&:-webkit-autofill_selected]:bg-transparent`}
|
||||
type={type}
|
||||
id={id}
|
||||
name={name}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import mermaid from 'mermaid';
|
||||
import CopyButton from './CopyButton';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
@@ -15,6 +16,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
||||
code,
|
||||
isLoading,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
const diagramId = useRef(
|
||||
`mermaid-${Date.now()}-${Math.random().toString(36).substring(2)}`,
|
||||
@@ -273,7 +275,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
||||
<button
|
||||
onClick={() => setShowDownloadMenu(!showDownloadMenu)}
|
||||
className="flex h-full items-center rounded-sm bg-gray-100 px-2 py-1 text-xs dark:bg-gray-700"
|
||||
title="Download options"
|
||||
title={t('mermaid.downloadOptions')}
|
||||
>
|
||||
Download <span className="ml-1">▼</span>
|
||||
</button>
|
||||
@@ -307,7 +309,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
||||
? 'bg-blue-200 dark:bg-blue-800'
|
||||
: 'bg-gray-100 dark:bg-gray-700'
|
||||
}`}
|
||||
title="View Code"
|
||||
title={t('mermaid.viewCode')}
|
||||
>
|
||||
Code
|
||||
</button>
|
||||
@@ -353,7 +355,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
||||
setZoomFactor((prev) => Math.max(1, prev - 0.5))
|
||||
}
|
||||
className="rounded px-1 hover:bg-gray-600"
|
||||
title="Decrease zoom"
|
||||
title={t('mermaid.decreaseZoom')}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
@@ -362,7 +364,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
||||
onClick={() => {
|
||||
setZoomFactor(2);
|
||||
}}
|
||||
title="Reset zoom"
|
||||
title={t('mermaid.resetZoom')}
|
||||
>
|
||||
{zoomFactor.toFixed(1)}x
|
||||
</span>
|
||||
@@ -371,7 +373,7 @@ const MermaidRenderer: React.FC<MermaidRendererProps> = ({
|
||||
setZoomFactor((prev) => Math.min(6, prev + 0.5))
|
||||
}
|
||||
className="rounded px-1 hover:bg-gray-600"
|
||||
title="Increase zoom"
|
||||
title={t('mermaid.increaseZoom')}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
removeAttachment,
|
||||
selectAttachments,
|
||||
updateAttachment,
|
||||
reorderAttachments,
|
||||
} from '../upload/uploadSlice';
|
||||
import { reorderAttachments } from '../upload/uploadSlice';
|
||||
|
||||
import { ActiveState } from '../models/misc';
|
||||
import {
|
||||
@@ -77,7 +77,7 @@ export default function MessageInput({
|
||||
(browserOS === 'mac' && event.metaKey && event.key === 'k')
|
||||
) {
|
||||
event.preventDefault();
|
||||
setIsSourcesPopupOpen(!isSourcesPopupOpen);
|
||||
setIsSourcesPopupOpen((s) => !s);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -89,8 +89,198 @@ export default function MessageInput({
|
||||
|
||||
const uploadFiles = useCallback(
|
||||
(files: File[]) => {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const apiHost = import.meta.env.VITE_API_HOST;
|
||||
|
||||
if (files.length > 1) {
|
||||
const formData = new FormData();
|
||||
const indexToUiId: Record<number, string> = {};
|
||||
|
||||
files.forEach((file, i) => {
|
||||
formData.append('file', file);
|
||||
const uiId = crypto.randomUUID();
|
||||
indexToUiId[i] = uiId;
|
||||
dispatch(
|
||||
addAttachment({
|
||||
id: uiId,
|
||||
fileName: file.name,
|
||||
progress: 0,
|
||||
status: 'uploading' as const,
|
||||
taskId: '',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
if (event.lengthComputable) {
|
||||
const progress = Math.round((event.loaded / event.total) * 100);
|
||||
Object.values(indexToUiId).forEach((uiId) =>
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
id: uiId,
|
||||
updates: { progress },
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.onload = () => {
|
||||
const status = xhr.status;
|
||||
if (status === 200) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
|
||||
if (Array.isArray(response?.tasks)) {
|
||||
const tasks = response.tasks as Array<{
|
||||
task_id?: string;
|
||||
filename?: string;
|
||||
attachment_id?: string;
|
||||
path?: string;
|
||||
}>;
|
||||
|
||||
tasks.forEach((t, idx) => {
|
||||
const uiId = indexToUiId[idx];
|
||||
if (!uiId) return;
|
||||
if (t?.task_id) {
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
id: uiId,
|
||||
updates: {
|
||||
taskId: t.task_id,
|
||||
status: 'processing',
|
||||
progress: 10,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
id: uiId,
|
||||
updates: { status: 'failed' },
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (tasks.length < files.length) {
|
||||
for (let i = tasks.length; i < files.length; i++) {
|
||||
const uiId = indexToUiId[i];
|
||||
if (uiId) {
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
id: uiId,
|
||||
updates: { status: 'failed' },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (response?.task_id) {
|
||||
if (files.length === 1) {
|
||||
const uiId = indexToUiId[0];
|
||||
if (uiId) {
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
id: uiId,
|
||||
updates: {
|
||||
taskId: response.task_id,
|
||||
status: 'processing',
|
||||
progress: 10,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
'Server returned a single task_id for multiple files. Update backend to return tasks[].',
|
||||
);
|
||||
const firstUi = indexToUiId[0];
|
||||
if (firstUi) {
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
id: firstUi,
|
||||
updates: {
|
||||
taskId: response.task_id,
|
||||
status: 'processing',
|
||||
progress: 10,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
for (let i = 1; i < files.length; i++) {
|
||||
const uiId = indexToUiId[i];
|
||||
if (uiId) {
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
id: uiId,
|
||||
updates: { status: 'failed' },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('Unexpected upload response shape', response);
|
||||
Object.values(indexToUiId).forEach((id) =>
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
id,
|
||||
updates: { status: 'failed' },
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Failed to parse upload response',
|
||||
err,
|
||||
xhr.responseText,
|
||||
);
|
||||
Object.values(indexToUiId).forEach((id) =>
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
id,
|
||||
updates: { status: 'failed' },
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.error('Upload failed', status, xhr.responseText);
|
||||
Object.values(indexToUiId).forEach((id) =>
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
id,
|
||||
updates: { status: 'failed' },
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
console.error('Upload network error');
|
||||
Object.values(indexToUiId).forEach((id) =>
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
id,
|
||||
updates: { status: 'failed' },
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
xhr.open('POST', `${apiHost}${endpoints.USER.STORE_ATTACHMENT}`);
|
||||
if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`);
|
||||
xhr.send(formData);
|
||||
return;
|
||||
}
|
||||
|
||||
// Single-file path: upload each file individually (original repo behavior)
|
||||
files.forEach((file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
@@ -121,16 +311,54 @@ export default function MessageInput({
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
if (response.task_id) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
if (response.task_id) {
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
id: uniqueId,
|
||||
updates: {
|
||||
taskId: response.task_id,
|
||||
status: 'processing',
|
||||
progress: 10,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// If backend returned tasks[] for single-file, handle gracefully:
|
||||
if (
|
||||
Array.isArray(response?.tasks) &&
|
||||
response.tasks[0]?.task_id
|
||||
) {
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
id: uniqueId,
|
||||
updates: {
|
||||
taskId: response.tasks[0].task_id,
|
||||
status: 'processing',
|
||||
progress: 10,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
id: uniqueId,
|
||||
updates: { status: 'failed' },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Failed to parse upload response',
|
||||
err,
|
||||
xhr.responseText,
|
||||
);
|
||||
dispatch(
|
||||
updateAttachment({
|
||||
id: uniqueId,
|
||||
updates: {
|
||||
taskId: response.task_id,
|
||||
status: 'processing',
|
||||
progress: 10,
|
||||
},
|
||||
updates: { status: 'failed' },
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -154,7 +382,7 @@ export default function MessageInput({
|
||||
};
|
||||
|
||||
xhr.open('POST', `${apiHost}${endpoints.USER.STORE_ATTACHMENT}`);
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
|
||||
if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`);
|
||||
xhr.send(formData);
|
||||
});
|
||||
},
|
||||
@@ -163,15 +391,13 @@ export default function MessageInput({
|
||||
|
||||
const handleFileAttachment = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files || e.target.files.length === 0) return;
|
||||
|
||||
const files = Array.from(e.target.files);
|
||||
uploadFiles(files);
|
||||
|
||||
// clear input so same file can be selected again
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
// Drag and drop handler
|
||||
// Drag & drop via react-dropzone
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
uploadFiles(acceptedFiles);
|
||||
@@ -321,11 +547,8 @@ export default function MessageInput({
|
||||
handleAbort();
|
||||
};
|
||||
|
||||
// Drag state for reordering
|
||||
const [draggingId, setDraggingId] = useState<string | null>(null);
|
||||
|
||||
// no preview object URLs to revoke (preview removed per reviewer request)
|
||||
|
||||
const findIndexById = (id: string) =>
|
||||
attachments.findIndex((a) => a.id === id);
|
||||
|
||||
@@ -359,7 +582,9 @@ export default function MessageInput({
|
||||
|
||||
return (
|
||||
<div {...getRootProps()} className="flex w-full flex-col">
|
||||
{/* react-dropzone input (for drag/drop) */}
|
||||
<input {...getInputProps()} />
|
||||
|
||||
<div className="border-dark-gray bg-lotion dark:border-grey relative flex w-full flex-col rounded-[23px] border dark:bg-transparent">
|
||||
<div className="flex flex-wrap gap-1.5 px-2 py-2 sm:gap-2 sm:px-3">
|
||||
{attachments.map((attachment) => {
|
||||
@@ -374,7 +599,11 @@ export default function MessageInput({
|
||||
attachment.status !== 'completed'
|
||||
? 'opacity-70'
|
||||
: 'opacity-100'
|
||||
} ${draggingId === attachment.id ? 'ring-dashed opacity-60 ring-2 ring-purple-200' : ''}`}
|
||||
} ${
|
||||
draggingId === attachment.id
|
||||
? 'ring-dashed opacity-60 ring-2 ring-purple-200'
|
||||
: ''
|
||||
}`}
|
||||
title={attachment.fileName}
|
||||
>
|
||||
<div className="bg-purple-30 mr-2 flex h-8 w-8 items-center justify-center rounded-md p-1">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import close from '../assets/cross.svg';
|
||||
import rightArrow from '../assets/arrow-full-right.svg';
|
||||
import bg from '../assets/notification-bg.jpg';
|
||||
@@ -13,13 +14,14 @@ export default function Notification({
|
||||
notificationLink,
|
||||
handleCloseNotification,
|
||||
}: NotificationProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<a
|
||||
className="absolute right-2 bottom-6 z-20 flex w-3/4 items-center justify-center gap-2 rounded-lg bg-cover bg-center bg-no-repeat px-2 py-4 sm:right-4 md:w-2/5 lg:w-1/3 xl:w-1/4 2xl:w-1/5"
|
||||
style={{ backgroundImage: `url(${bg})` }}
|
||||
href={notificationLink}
|
||||
target="_blank"
|
||||
aria-label="Notification"
|
||||
aria-label={t('notification.ariaLabel')}
|
||||
rel="noreferrer"
|
||||
>
|
||||
<p className="text-white-3000 text-xs leading-6 font-semibold xl:text-sm xl:leading-7">
|
||||
@@ -31,7 +33,7 @@ export default function Notification({
|
||||
|
||||
<button
|
||||
className="absolute top-2 right-2 z-30 h-4 w-4 hover:opacity-70"
|
||||
aria-label="Close notification"
|
||||
aria-label={t('notification.closeAriaLabel')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
@@ -24,6 +24,7 @@ interface SettingsBarProps {
|
||||
}
|
||||
|
||||
const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [hiddenGradient, setHiddenGradient] =
|
||||
useState<HiddenGradientType>('left');
|
||||
const containerRef = useRef<null | HTMLDivElement>(null);
|
||||
@@ -60,7 +61,7 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
|
||||
<button
|
||||
onClick={() => scrollTabs(-1)}
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full transition-all hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
aria-label="Scroll tabs left"
|
||||
aria-label={t('settings.scrollTabsLeft')}
|
||||
>
|
||||
<img src={ArrowLeft} alt="left-arrow" className="h-3" />
|
||||
</button>
|
||||
@@ -69,7 +70,7 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
|
||||
ref={containerRef}
|
||||
className="no-scrollbar flex snap-x flex-nowrap overflow-x-auto scroll-smooth md:space-x-4"
|
||||
role="tablist"
|
||||
aria-label="Settings tabs"
|
||||
aria-label={t('settings.tabsAriaLabel')}
|
||||
>
|
||||
{tabs.map((tab, index) => (
|
||||
<button
|
||||
@@ -93,7 +94,7 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
|
||||
<button
|
||||
onClick={() => scrollTabs(1)}
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
aria-label="Scroll tabs right"
|
||||
aria-label={t('settings.scrollTabsRight')}
|
||||
>
|
||||
<img src={ArrowRight} alt="right-arrow" className="h-3" />
|
||||
</button>
|
||||
|
||||
@@ -172,11 +172,7 @@ export default function SourcesPopup({
|
||||
: doc.date !== option.date,
|
||||
)
|
||||
: [];
|
||||
dispatch(
|
||||
setSelectedDocs(
|
||||
updatedDocs.length > 0 ? updatedDocs : null,
|
||||
),
|
||||
);
|
||||
dispatch(setSelectedDocs(updatedDocs));
|
||||
handlePostDocumentSelect(
|
||||
updatedDocs.length > 0 ? updatedDocs : null,
|
||||
);
|
||||
|
||||
@@ -1,94 +1,202 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import Speaker from '../assets/speaker.svg?react';
|
||||
import Stopspeech from '../assets/stopspeech.svg?react';
|
||||
import LoadingIcon from '../assets/Loading.svg?react'; // Add a loading icon SVG here
|
||||
|
||||
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
|
||||
|
||||
export default function SpeakButton({
|
||||
text,
|
||||
colorLight,
|
||||
colorDark,
|
||||
}: {
|
||||
text: string;
|
||||
colorLight?: string;
|
||||
colorDark?: string;
|
||||
}) {
|
||||
let currentlyPlayingAudio: {
|
||||
audio: HTMLAudioElement;
|
||||
stopCallback: () => void;
|
||||
} | null = null;
|
||||
|
||||
let currentLoadingRequest: {
|
||||
abortController: AbortController;
|
||||
stopLoadingCallback: () => void;
|
||||
} | null = null;
|
||||
|
||||
// LRU Cache for audio
|
||||
const audioCache = new Map<string, string>();
|
||||
const MAX_CACHE_SIZE = 10;
|
||||
|
||||
function getCachedAudio(text: string): string | undefined {
|
||||
const cached = audioCache.get(text);
|
||||
if (cached) {
|
||||
audioCache.delete(text);
|
||||
audioCache.set(text, cached);
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
function setCachedAudio(text: string, audioBase64: string) {
|
||||
if (audioCache.has(text)) {
|
||||
audioCache.delete(text);
|
||||
}
|
||||
if (audioCache.size >= MAX_CACHE_SIZE) {
|
||||
const firstKey = audioCache.keys().next().value;
|
||||
if (firstKey !== undefined) {
|
||||
audioCache.delete(firstKey);
|
||||
}
|
||||
}
|
||||
|
||||
audioCache.set(text, audioBase64);
|
||||
}
|
||||
|
||||
export default function SpeakButton({ text }: { text: string }) {
|
||||
const [isSpeaking, setIsSpeaking] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSpeakHovered, setIsSpeakHovered] = useState(false);
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Abort any pending fetch request
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
|
||||
// Stop any playing audio
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
if (currentlyPlayingAudio?.audio === audioRef.current) {
|
||||
currentlyPlayingAudio = null;
|
||||
}
|
||||
audioRef.current = null;
|
||||
}
|
||||
|
||||
// Clear global loading request if it's this component's
|
||||
if (currentLoadingRequest) {
|
||||
currentLoadingRequest = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSpeakClick = async () => {
|
||||
if (isSpeaking) {
|
||||
// Stop audio if it's currently playing
|
||||
audioRef.current?.pause();
|
||||
audioRef.current = null;
|
||||
currentlyPlayingAudio = null;
|
||||
setIsSpeaking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop any currently playing audio
|
||||
if (currentlyPlayingAudio) {
|
||||
currentlyPlayingAudio.audio.pause();
|
||||
currentlyPlayingAudio.stopCallback();
|
||||
currentlyPlayingAudio = null;
|
||||
}
|
||||
|
||||
// Abort any pending loading request
|
||||
if (currentLoadingRequest) {
|
||||
currentLoadingRequest.abortController.abort();
|
||||
currentLoadingRequest.stopLoadingCallback();
|
||||
currentLoadingRequest = null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Set loading state and initiate TTS request
|
||||
setIsLoading(true);
|
||||
const cachedAudio = getCachedAudio(text);
|
||||
let audioBase64: string;
|
||||
|
||||
const response = await fetch(apiHost + '/api/tts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.audio_base64) {
|
||||
// Create and play the audio
|
||||
const audio = new Audio(`data:audio/mp3;base64,${data.audio_base64}`);
|
||||
audioRef.current = audio;
|
||||
|
||||
audio.play().then(() => {
|
||||
setIsSpeaking(true);
|
||||
setIsLoading(false);
|
||||
|
||||
// Reset when audio ends
|
||||
audio.onended = () => {
|
||||
setIsSpeaking(false);
|
||||
audioRef.current = null;
|
||||
};
|
||||
});
|
||||
} else {
|
||||
console.error('Failed to retrieve audio.');
|
||||
if (cachedAudio) {
|
||||
audioBase64 = cachedAudio;
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
|
||||
currentLoadingRequest = {
|
||||
abortController,
|
||||
stopLoadingCallback: () => {
|
||||
setIsLoading(false);
|
||||
},
|
||||
};
|
||||
|
||||
const response = await fetch(apiHost + '/api/tts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text }),
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
abortControllerRef.current = null;
|
||||
currentLoadingRequest = null;
|
||||
|
||||
if (data.success && data.audio_base64) {
|
||||
audioBase64 = data.audio_base64;
|
||||
// Store in cache
|
||||
setCachedAudio(text, audioBase64);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
console.error('Failed to retrieve audio.');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const audio = new Audio(`data:audio/mp3;base64,${audioBase64}`);
|
||||
audioRef.current = audio;
|
||||
|
||||
currentlyPlayingAudio = {
|
||||
audio,
|
||||
stopCallback: () => {
|
||||
setIsSpeaking(false);
|
||||
audioRef.current = null;
|
||||
},
|
||||
};
|
||||
|
||||
audio.play().then(() => {
|
||||
setIsSpeaking(true);
|
||||
setIsLoading(false);
|
||||
|
||||
audio.onended = () => {
|
||||
setIsSpeaking(false);
|
||||
audioRef.current = null;
|
||||
if (currentlyPlayingAudio?.audio === audio) {
|
||||
currentlyPlayingAudio = null;
|
||||
}
|
||||
};
|
||||
});
|
||||
} catch (error: any) {
|
||||
abortControllerRef.current = null;
|
||||
currentLoadingRequest = null;
|
||||
|
||||
if (error.name === 'AbortError') {
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching audio from TTS endpoint', error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-center rounded-full p-2 ${
|
||||
isSpeakHovered
|
||||
? `dark:bg-purple-taupe bg-[#EEEEEE]`
|
||||
: `bg-[${colorLight ? colorLight : '#FFFFFF'}] dark:bg-[${colorDark ? colorDark : 'transparent'}]`
|
||||
<button
|
||||
type="button"
|
||||
className={`flex cursor-pointer items-center justify-center rounded-full p-2 ${
|
||||
isSpeaking || isLoading
|
||||
? 'dark:bg-purple-taupe bg-[#EEEEEE]'
|
||||
: 'bg-white-3000 dark:hover:bg-purple-taupe hover:bg-[#EEEEEE] dark:bg-transparent'
|
||||
}`}
|
||||
onClick={handleSpeakClick}
|
||||
aria-label={
|
||||
isLoading
|
||||
? 'Loading audio'
|
||||
: isSpeaking
|
||||
? 'Stop speaking'
|
||||
: 'Speak text'
|
||||
}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoadingIcon className="animate-spin" />
|
||||
) : isSpeaking ? (
|
||||
<Stopspeech
|
||||
className="cursor-pointer fill-none"
|
||||
onClick={handleSpeakClick}
|
||||
onMouseEnter={() => setIsSpeakHovered(true)}
|
||||
onMouseLeave={() => setIsSpeakHovered(false)}
|
||||
/>
|
||||
<Stopspeech className="fill-none" />
|
||||
) : (
|
||||
<Speaker
|
||||
className="cursor-pointer fill-none"
|
||||
onClick={handleSpeakClick}
|
||||
onMouseEnter={() => setIsSpeakHovered(true)}
|
||||
onMouseLeave={() => setIsSpeakHovered(false)}
|
||||
/>
|
||||
<Speaker className="fill-none" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export type InputProps = {
|
||||
e: React.KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>,
|
||||
) => void;
|
||||
leftIcon?: React.ReactNode;
|
||||
edgeRoundness?: string;
|
||||
};
|
||||
|
||||
export type MermaidRendererProps = {
|
||||
|
||||
@@ -130,7 +130,7 @@ export default function Conversation() {
|
||||
}),
|
||||
);
|
||||
handleQuestion({
|
||||
question: queries[queries.length - 1].prompt,
|
||||
question: question,
|
||||
isRetry: true,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -560,37 +560,47 @@ const ConversationBubble = forwardRef<
|
||||
{handleFeedback && (
|
||||
<>
|
||||
<div className="relative mr-2 flex items-center justify-center">
|
||||
<div>
|
||||
<div className="bg-white-3000 dark:hover:bg-purple-taupe flex items-center justify-center rounded-full p-2 hover:bg-[#EEEEEE] dark:bg-transparent">
|
||||
<Like
|
||||
className={`${feedback === 'LIKE' ? 'fill-white-3000 stroke-purple-30 dark:fill-transparent' : 'stroke-gray-4000 fill-none'} cursor-pointer`}
|
||||
onClick={() => {
|
||||
if (feedback === 'LIKE') {
|
||||
handleFeedback?.(null);
|
||||
} else {
|
||||
handleFeedback?.('LIKE');
|
||||
}
|
||||
}}
|
||||
></Like>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-white-3000 dark:hover:bg-purple-taupe flex cursor-pointer items-center justify-center rounded-full p-2 hover:bg-[#EEEEEE] dark:bg-transparent"
|
||||
onClick={() => {
|
||||
if (feedback === 'LIKE') {
|
||||
handleFeedback?.(null);
|
||||
} else {
|
||||
handleFeedback?.('LIKE');
|
||||
}
|
||||
}}
|
||||
aria-label={
|
||||
feedback === 'LIKE' ? 'Remove like' : 'Like'
|
||||
}
|
||||
>
|
||||
<Like
|
||||
className={`${feedback === 'LIKE' ? 'fill-white-3000 stroke-purple-30 dark:fill-transparent' : 'stroke-gray-4000 fill-none'}`}
|
||||
></Like>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="relative mr-2 flex items-center justify-center">
|
||||
<div>
|
||||
<div className="bg-white-3000 dark:hover:bg-purple-taupe flex items-center justify-center rounded-full p-2 hover:bg-[#EEEEEE] dark:bg-transparent">
|
||||
<Dislike
|
||||
className={`${feedback === 'DISLIKE' ? 'fill-white-3000 stroke-red-2000 dark:fill-transparent' : 'stroke-gray-4000 fill-none'} cursor-pointer`}
|
||||
onClick={() => {
|
||||
if (feedback === 'DISLIKE') {
|
||||
handleFeedback?.(null);
|
||||
} else {
|
||||
handleFeedback?.('DISLIKE');
|
||||
}
|
||||
}}
|
||||
></Dislike>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-white-3000 dark:hover:bg-purple-taupe flex cursor-pointer items-center justify-center rounded-full p-2 hover:bg-[#EEEEEE] dark:bg-transparent"
|
||||
onClick={() => {
|
||||
if (feedback === 'DISLIKE') {
|
||||
handleFeedback?.(null);
|
||||
} else {
|
||||
handleFeedback?.('DISLIKE');
|
||||
}
|
||||
}}
|
||||
aria-label={
|
||||
feedback === 'DISLIKE'
|
||||
? 'Remove dislike'
|
||||
: 'Dislike'
|
||||
}
|
||||
>
|
||||
<Dislike
|
||||
className={`${feedback === 'DISLIKE' ? 'fill-white-3000 stroke-red-2000 dark:fill-transparent' : 'stroke-gray-4000 fill-none'}`}
|
||||
></Dislike>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -793,6 +803,7 @@ function Thought({
|
||||
thought: string;
|
||||
preprocessLaTeX: (content: string) => string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
const [isThoughtOpen, setIsThoughtOpen] = useState(true);
|
||||
|
||||
@@ -813,7 +824,9 @@ function Thought({
|
||||
className="flex flex-row items-center gap-2"
|
||||
onClick={() => setIsThoughtOpen(!isThoughtOpen)}
|
||||
>
|
||||
<p className="text-base font-semibold">Reasoning</p>
|
||||
<p className="text-base font-semibold">
|
||||
{t('conversation.reasoning')}
|
||||
</p>
|
||||
<img
|
||||
src={ChevronDown}
|
||||
alt="ChevronDown"
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import ArrowDown from '../assets/arrow-down.svg';
|
||||
import RetryIcon from '../components/RetryIcon';
|
||||
@@ -14,6 +15,7 @@ import Hero from '../Hero';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
import ConversationBubble from './ConversationBubble';
|
||||
import { FEEDBACK, Query, Status } from './conversationModels';
|
||||
import { selectConversationId } from '../preferences/preferenceSlice';
|
||||
|
||||
const SCROLL_THRESHOLD = 10;
|
||||
const LAST_BUBBLE_MARGIN = 'mb-32';
|
||||
@@ -50,6 +52,7 @@ export default function ConversationMessages({
|
||||
}: ConversationMessagesProps) {
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
const { t } = useTranslation();
|
||||
const conversationId = useSelector(selectConversationId);
|
||||
|
||||
const conversationRef = useRef<HTMLDivElement>(null);
|
||||
const [hasScrolledToLast, setHasScrolledToLast] = useState(true);
|
||||
@@ -87,15 +90,20 @@ export default function ConversationMessages({
|
||||
setHasScrolledToLast(isAtBottom);
|
||||
}, [setHasScrolledToLast]);
|
||||
|
||||
const lastQuery = queries[queries.length - 1];
|
||||
const lastQueryResponse = lastQuery?.response;
|
||||
const lastQueryError = lastQuery?.error;
|
||||
const lastQueryThought = lastQuery?.thought;
|
||||
|
||||
useEffect(() => {
|
||||
if (!userInterruptedScroll) {
|
||||
scrollConversationToBottom();
|
||||
}
|
||||
}, [
|
||||
queries.length,
|
||||
queries[queries.length - 1]?.response,
|
||||
queries[queries.length - 1]?.error,
|
||||
queries[queries.length - 1]?.thought,
|
||||
lastQueryResponse,
|
||||
lastQueryError,
|
||||
lastQueryThought,
|
||||
userInterruptedScroll,
|
||||
scrollConversationToBottom,
|
||||
]);
|
||||
@@ -137,7 +145,7 @@ export default function ConversationMessages({
|
||||
return (
|
||||
<ConversationBubble
|
||||
className={bubbleMargin}
|
||||
key={`${index}-ANSWER`}
|
||||
key={`${conversationId}-${index}-ANSWER`}
|
||||
message={query.response}
|
||||
type={'ANSWER'}
|
||||
thought={query.thought}
|
||||
@@ -175,7 +183,7 @@ export default function ConversationMessages({
|
||||
return (
|
||||
<ConversationBubble
|
||||
className={bubbleMargin}
|
||||
key={`${index}-ERROR`}
|
||||
key={`${conversationId}-${index}-ERROR`}
|
||||
message={query.error}
|
||||
type="ERROR"
|
||||
retryBtn={retryButton}
|
||||
@@ -214,10 +222,10 @@ export default function ConversationMessages({
|
||||
|
||||
{queries.length > 0 ? (
|
||||
queries.map((query, index) => (
|
||||
<Fragment key={`${index}-query-fragment`}>
|
||||
<Fragment key={`${conversationId}-${index}-query-fragment`}>
|
||||
<ConversationBubble
|
||||
className={index === 0 ? FIRST_QUESTION_BUBBLE_MARGIN_TOP : ''}
|
||||
key={`${index}-QUESTION`}
|
||||
key={`${conversationId}-${index}-QUESTION`}
|
||||
message={query.prompt}
|
||||
type="QUESTION"
|
||||
handleUpdatedQuestionSubmission={handleQuestionSubmission}
|
||||
|
||||
@@ -370,7 +370,10 @@ export const conversationSlice = createSlice({
|
||||
return state;
|
||||
}
|
||||
state.status = 'failed';
|
||||
state.queries[state.queries.length - 1].error = 'Something went wrong';
|
||||
if (state.queries.length > 0) {
|
||||
state.queries[state.queries.length - 1].error =
|
||||
'Something went wrong';
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -225,6 +225,16 @@ layer(base);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
.prompt-variable-highlight {
|
||||
background-color: rgba(106, 77, 244, 0.18);
|
||||
border-radius: 0.375rem;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
.dark .prompt-variable-highlight {
|
||||
background-color: rgba(106, 77, 244, 0.32);
|
||||
}
|
||||
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
|
||||
@@ -201,6 +201,15 @@
|
||||
"noAuth": "No Authentication",
|
||||
"oauthInProgress": "Waiting for OAuth completion...",
|
||||
"oauthCompleted": "OAuth completed successfully",
|
||||
"authType": "Authentication Type",
|
||||
"defaultServerName": "My MCP Server",
|
||||
"authTypes": {
|
||||
"none": "No Authentication",
|
||||
"apiKey": "API Key",
|
||||
"bearer": "Bearer Token",
|
||||
"oauth": "OAuth",
|
||||
"basic": "Basic Authentication"
|
||||
},
|
||||
"placeholders": {
|
||||
"serverUrl": "https://api.example.com",
|
||||
"apiKey": "Your secret API key",
|
||||
@@ -220,10 +229,14 @@
|
||||
"testFailed": "Connection test failed",
|
||||
"saveFailed": "Failed to save MCP server",
|
||||
"oauthFailed": "OAuth process failed or was cancelled",
|
||||
"oauthTimeout": "OAuth process timed out, please try again"
|
||||
"oauthTimeout": "OAuth process timed out, please try again",
|
||||
"timeoutRange": "Timeout must be between 1 and 300 seconds"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"tabsAriaLabel": "Settings tabs",
|
||||
"scrollTabsRight": "Scroll tabs right"
|
||||
},
|
||||
"modals": {
|
||||
"uploadDoc": {
|
||||
@@ -343,7 +356,8 @@
|
||||
"disclaimer": "This is the only time your key will be shown.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"confirm": "I saved the Key"
|
||||
"confirm": "I saved the Key",
|
||||
"apiKeyLabel": "API Key"
|
||||
},
|
||||
"deleteConv": {
|
||||
"confirm": "Are you sure you want to delete all the conversations?",
|
||||
@@ -361,7 +375,8 @@
|
||||
"apiKeyLabel": "API Key / OAuth",
|
||||
"apiKeyPlaceholder": "Enter API Key / OAuth",
|
||||
"addButton": "Add Tool",
|
||||
"closeButton": "Close"
|
||||
"closeButton": "Close",
|
||||
"customNamePlaceholder": "Enter custom name (optional)"
|
||||
},
|
||||
"prompts": {
|
||||
"addPrompt": "Add Prompt",
|
||||
@@ -371,8 +386,32 @@
|
||||
"promptName": "Prompt Name",
|
||||
"promptText": "Prompt Text",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"nameExists": "Name already exists",
|
||||
"deleteConfirmation": "Are you sure you want to delete the prompt '{{name}}'?"
|
||||
"deleteConfirmation": "Are you sure you want to delete the prompt '{{name}}'?",
|
||||
"placeholderText": "Type your prompt text here...",
|
||||
"addExamplePlaceholder": "Please summarize this text:",
|
||||
"variablesLabel": "Variables",
|
||||
"variablesSubtext": "Click To Insert Into Prompt",
|
||||
"variablesDescription": "Click to insert into prompt",
|
||||
"systemVariables": "Click to insert into prompt",
|
||||
"toolVariables": "Tool Variables",
|
||||
"systemVariablesDropdownLabel": "System Variables",
|
||||
"systemVariableOptions": {
|
||||
"sourceContent": "Sources content",
|
||||
"sourceSummaries": "Alias for content (backward compatible)",
|
||||
"sourceDocuments": "Document objects list",
|
||||
"sourceCount": "Number of retrieved documents",
|
||||
"systemDate": "Current date (YYYY-MM-DD)",
|
||||
"systemTime": "Current time (HH:MM:SS)",
|
||||
"systemTimestamp": "ISO 8601 timestamp",
|
||||
"systemRequestId": "Unique request identifier",
|
||||
"systemUserId": "Current user ID"
|
||||
},
|
||||
"learnAboutPrompts": "Learn about Prompts →",
|
||||
"publicPromptEditDisabled": "Public prompts cannot be edited",
|
||||
"promptTypePublic": "public",
|
||||
"promptTypePrivate": "private"
|
||||
},
|
||||
"chunk": {
|
||||
"add": "Add Chunk",
|
||||
@@ -386,6 +425,22 @@
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"deleteConfirmation": "Are you sure you want to delete this chunk?"
|
||||
},
|
||||
"addAction": {
|
||||
"title": "New Action",
|
||||
"actionNamePlaceholder": "Action Name",
|
||||
"invalidFormat": "Invalid function name format. Use only letters, numbers, underscores, and hyphens.",
|
||||
"formatHelp": "Use only letters, numbers, underscores, and hyphens (e.g., `get_data`, `send_report`, etc.)",
|
||||
"addButton": "Add"
|
||||
},
|
||||
"agentDetails": {
|
||||
"title": "Access Details",
|
||||
"publicLink": "Public Link",
|
||||
"apiKey": "API Key",
|
||||
"webhookUrl": "Webhook URL",
|
||||
"generate": "Generate",
|
||||
"test": "Test",
|
||||
"learnMore": "Learn more"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
@@ -428,6 +483,153 @@
|
||||
"attach": "Attach",
|
||||
"remove": "Remove attachment"
|
||||
},
|
||||
"retry": "Retry"
|
||||
"retry": "Retry",
|
||||
"reasoning": "Reasoning"
|
||||
},
|
||||
"agents": {
|
||||
"title": "Agents",
|
||||
"description": "Discover and create custom versions of DocsGPT that combine instructions, extra knowledge, and any combination of skills",
|
||||
"newAgent": "New Agent",
|
||||
"backToAll": "Back to all agents",
|
||||
"sections": {
|
||||
"template": {
|
||||
"title": "By DocsGPT",
|
||||
"description": "Agents provided by DocsGPT",
|
||||
"emptyState": "No template agents found."
|
||||
},
|
||||
"user": {
|
||||
"title": "By me",
|
||||
"description": "Agents created or published by you",
|
||||
"emptyState": "You don't have any created agents yet."
|
||||
},
|
||||
"shared": {
|
||||
"title": "Shared with me",
|
||||
"description": "Agents imported by using a public link",
|
||||
"emptyState": "No shared agents found."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"headings": {
|
||||
"new": "New Agent",
|
||||
"edit": "Edit Agent",
|
||||
"draft": "New Agent (Draft)"
|
||||
},
|
||||
"buttons": {
|
||||
"publish": "Publish",
|
||||
"save": "Save",
|
||||
"saveDraft": "Save Draft",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"logs": "Logs",
|
||||
"accessDetails": "Access Details",
|
||||
"add": "Add"
|
||||
},
|
||||
"sections": {
|
||||
"meta": "Meta",
|
||||
"source": "Source",
|
||||
"prompt": "Prompt",
|
||||
"tools": "Tools",
|
||||
"agentType": "Agent type",
|
||||
"advanced": "Advanced",
|
||||
"preview": "Preview"
|
||||
},
|
||||
"placeholders": {
|
||||
"agentName": "Agent name",
|
||||
"describeAgent": "Describe your agent",
|
||||
"selectSources": "Select sources",
|
||||
"chunksPerQuery": "Chunks per query",
|
||||
"selectType": "Select type",
|
||||
"selectTools": "Select tools",
|
||||
"enterTokenLimit": "Enter token limit",
|
||||
"enterRequestLimit": "Enter request limit"
|
||||
},
|
||||
"sourcePopup": {
|
||||
"title": "Select Sources",
|
||||
"searchPlaceholder": "Search sources...",
|
||||
"noOptionsMessage": "No sources available"
|
||||
},
|
||||
"toolsPopup": {
|
||||
"title": "Select Tools",
|
||||
"searchPlaceholder": "Search tools...",
|
||||
"noOptionsMessage": "No tools available"
|
||||
},
|
||||
"upload": {
|
||||
"clickToUpload": "Click to upload",
|
||||
"dragAndDrop": " or drag and drop"
|
||||
},
|
||||
"agentTypes": {
|
||||
"classic": "Classic",
|
||||
"react": "ReAct"
|
||||
},
|
||||
"advanced": {
|
||||
"jsonSchema": "JSON response schema",
|
||||
"jsonSchemaDescription": "Define a JSON schema to enforce structured output format",
|
||||
"validJson": "Valid JSON",
|
||||
"invalidJson": "Invalid JSON - fix to enable saving",
|
||||
"tokenLimiting": "Token limiting",
|
||||
"tokenLimitingDescription": "Limit daily total tokens that can be used by this agent",
|
||||
"requestLimiting": "Request limiting",
|
||||
"requestLimitingDescription": "Limit daily total requests that can be made to this agent"
|
||||
},
|
||||
"preview": {
|
||||
"publishedPreview": "Published agents can be previewed here"
|
||||
},
|
||||
"externalKb": "External KB"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Agent Logs",
|
||||
"lastUsedAt": "Last used at",
|
||||
"noUsageHistory": "No usage history",
|
||||
"tableHeader": "Agent endpoint logs"
|
||||
},
|
||||
"shared": {
|
||||
"notFound": "No agent found. Please ensure the agent is shared."
|
||||
},
|
||||
"preview": {
|
||||
"testMessage": "Test your agent here. Published agents can be used in conversations."
|
||||
},
|
||||
"deleteConfirmation": "Are you sure you want to delete this agent?"
|
||||
},
|
||||
"components": {
|
||||
"fileUpload": {
|
||||
"clickToUpload": "Click to upload or drag and drop",
|
||||
"dropFiles": "Drop the files here",
|
||||
"fileTypes": "PNG, JPG, JPEG up to",
|
||||
"sizeLimitUnit": "MB",
|
||||
"fileSizeError": "File exceeds {{size}}MB limit"
|
||||
}
|
||||
},
|
||||
"pageNotFound": {
|
||||
"title": "404",
|
||||
"message": "The page you are looking for does not exist.",
|
||||
"goHome": "Go Back Home"
|
||||
},
|
||||
"filePicker": {
|
||||
"searchPlaceholder": "Search files and folders...",
|
||||
"itemsSelected": "{{count}} selected",
|
||||
"name": "Name",
|
||||
"lastModified": "Last Modified",
|
||||
"size": "Size"
|
||||
},
|
||||
"actionButtons": {
|
||||
"openNewChat": "Open New Chat",
|
||||
"share": "Share"
|
||||
},
|
||||
"mermaid": {
|
||||
"downloadOptions": "Download options",
|
||||
"viewCode": "View Code",
|
||||
"decreaseZoom": "Decrease zoom",
|
||||
"resetZoom": "Reset zoom",
|
||||
"increaseZoom": "Increase zoom"
|
||||
},
|
||||
"navigation": {
|
||||
"agents": "Agents"
|
||||
},
|
||||
"notification": {
|
||||
"ariaLabel": "Notification",
|
||||
"closeAriaLabel": "Close notification"
|
||||
},
|
||||
"prompts": {
|
||||
"textAriaLabel": "Prompt Text"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,8 +185,58 @@
|
||||
"fieldDescription": "Descripción del campo",
|
||||
"add": "Añadir",
|
||||
"cancel": "Cancelar",
|
||||
"addNew": "Añadir Nuevo"
|
||||
}
|
||||
"addNew": "Añadir Nuevo",
|
||||
"mcp": {
|
||||
"addServer": "Add MCP Server",
|
||||
"editServer": "Edit Server",
|
||||
"serverName": "Server Name",
|
||||
"serverUrl": "Server URL",
|
||||
"headerName": "Header Name",
|
||||
"timeout": "Timeout (seconds)",
|
||||
"testConnection": "Test Connection",
|
||||
"testing": "Testing",
|
||||
"saving": "Saving",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"noAuth": "No Authentication",
|
||||
"oauthInProgress": "Waiting for OAuth completion...",
|
||||
"oauthCompleted": "OAuth completed successfully",
|
||||
"authType": "Authentication Type",
|
||||
"defaultServerName": "My MCP Server",
|
||||
"authTypes": {
|
||||
"none": "No Authentication",
|
||||
"apiKey": "API Key",
|
||||
"bearer": "Bearer Token",
|
||||
"oauth": "OAuth",
|
||||
"basic": "Basic Authentication"
|
||||
},
|
||||
"placeholders": {
|
||||
"serverUrl": "https://api.example.com",
|
||||
"apiKey": "Your secret API key",
|
||||
"bearerToken": "Your secret token",
|
||||
"username": "Your username",
|
||||
"password": "Your password",
|
||||
"oauthScopes": "OAuth scopes (comma separated)"
|
||||
},
|
||||
"errors": {
|
||||
"nameRequired": "Server name is required",
|
||||
"urlRequired": "Server URL is required",
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"apiKeyRequired": "API key is required",
|
||||
"tokenRequired": "Bearer token is required",
|
||||
"usernameRequired": "Username is required",
|
||||
"passwordRequired": "Password is required",
|
||||
"testFailed": "Connection test failed",
|
||||
"saveFailed": "Failed to save MCP server",
|
||||
"oauthFailed": "OAuth process failed or was cancelled",
|
||||
"oauthTimeout": "OAuth process timed out, please try again",
|
||||
"timeoutRange": "Timeout must be between 1 and 300 seconds"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scrollTabsLeft": "Desplazar pestañas a la izquierda",
|
||||
"tabsAriaLabel": "Pestañas de configuración",
|
||||
"scrollTabsRight": "Desplazar pestañas a la derecha"
|
||||
},
|
||||
"modals": {
|
||||
"uploadDoc": {
|
||||
@@ -306,7 +356,8 @@
|
||||
"disclaimer": "Esta es la única vez que se mostrará tu clave.",
|
||||
"copy": "Copiar",
|
||||
"copied": "Copiado",
|
||||
"confirm": "He guardado la Clave"
|
||||
"confirm": "He guardado la Clave",
|
||||
"apiKeyLabel": "API Key"
|
||||
},
|
||||
"deleteConv": {
|
||||
"confirm": "¿Estás seguro de que deseas eliminar todas las conversaciones?",
|
||||
@@ -324,7 +375,8 @@
|
||||
"apiKeyLabel": "Clave API / OAuth",
|
||||
"apiKeyPlaceholder": "Ingrese la Clave API / OAuth",
|
||||
"addButton": "Agregar Herramienta",
|
||||
"closeButton": "Cerrar"
|
||||
"closeButton": "Cerrar",
|
||||
"customNamePlaceholder": "Enter custom name (optional)"
|
||||
},
|
||||
"prompts": {
|
||||
"addPrompt": "Agregar Prompt",
|
||||
@@ -334,8 +386,32 @@
|
||||
"promptName": "Nombre del Prompt",
|
||||
"promptText": "Texto del Prompt",
|
||||
"save": "Guardar",
|
||||
"cancel": "Cancelar",
|
||||
"nameExists": "El nombre ya existe",
|
||||
"deleteConfirmation": "¿Estás seguro de que deseas eliminar el prompt '{{name}}'?"
|
||||
"deleteConfirmation": "¿Estás seguro de que deseas eliminar el prompt '{{name}}'?",
|
||||
"placeholderText": "Escribe tu texto de prompt aquí...",
|
||||
"addExamplePlaceholder": "Por favor, resume este texto:",
|
||||
"variablesLabel": "Variables",
|
||||
"variablesSubtext": "Haz clic para insertar en el prompt",
|
||||
"variablesDescription": "Haz clic para insertar en el prompt",
|
||||
"systemVariables": "Variables del sistema",
|
||||
"toolVariables": "Variables de herramientas",
|
||||
"systemVariablesDropdownLabel": "Variables del sistema",
|
||||
"systemVariableOptions": {
|
||||
"sourceContent": "Contenido de las fuentes",
|
||||
"sourceSummaries": "Alias del contenido (compatibilidad retroactiva)",
|
||||
"sourceDocuments": "Lista de objetos de documentos",
|
||||
"sourceCount": "Número de documentos recuperados",
|
||||
"systemDate": "Fecha actual (YYYY-MM-DD)",
|
||||
"systemTime": "Hora actual (HH:MM:SS)",
|
||||
"systemTimestamp": "Marca de tiempo ISO 8601",
|
||||
"systemRequestId": "Identificador único de solicitud",
|
||||
"systemUserId": "ID del usuario actual"
|
||||
},
|
||||
"learnAboutPrompts": "Aprende sobre los Prompts →",
|
||||
"publicPromptEditDisabled": "Los prompts públicos no se pueden editar",
|
||||
"promptTypePublic": "público",
|
||||
"promptTypePrivate": "privado"
|
||||
},
|
||||
"chunk": {
|
||||
"add": "Agregar Fragmento",
|
||||
@@ -349,6 +425,22 @@
|
||||
"cancel": "Cancelar",
|
||||
"delete": "Eliminar",
|
||||
"deleteConfirmation": "¿Estás seguro de que deseas eliminar este fragmento?"
|
||||
},
|
||||
"addAction": {
|
||||
"title": "New Action",
|
||||
"actionNamePlaceholder": "Action Name",
|
||||
"invalidFormat": "Invalid function name format. Use only letters, numbers, underscores, and hyphens.",
|
||||
"formatHelp": "Use only letters, numbers, underscores, and hyphens (e.g., `get_data`, `send_report`, etc.)",
|
||||
"addButton": "Add"
|
||||
},
|
||||
"agentDetails": {
|
||||
"title": "Access Details",
|
||||
"publicLink": "Public Link",
|
||||
"apiKey": "API Key",
|
||||
"webhookUrl": "Webhook URL",
|
||||
"generate": "Generate",
|
||||
"test": "Test",
|
||||
"learnMore": "Learn more"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
@@ -391,6 +483,153 @@
|
||||
"attach": "Adjuntar",
|
||||
"remove": "Eliminar adjunto"
|
||||
},
|
||||
"retry": "Reintentar"
|
||||
"retry": "Reintentar",
|
||||
"reasoning": "Razonamiento"
|
||||
},
|
||||
"agents": {
|
||||
"title": "Agentes",
|
||||
"description": "Descubre y crea versiones personalizadas de DocsGPT que combinan instrucciones, conocimiento adicional y cualquier combinación de habilidades",
|
||||
"newAgent": "Nuevo Agente",
|
||||
"backToAll": "Volver a todos los agentes",
|
||||
"sections": {
|
||||
"template": {
|
||||
"title": "Por DocsGPT",
|
||||
"description": "Agentes proporcionados por DocsGPT",
|
||||
"emptyState": "No se encontraron agentes de plantilla."
|
||||
},
|
||||
"user": {
|
||||
"title": "Por mí",
|
||||
"description": "Agentes creados o publicados por ti",
|
||||
"emptyState": "Aún no tienes agentes creados."
|
||||
},
|
||||
"shared": {
|
||||
"title": "Compartidos conmigo",
|
||||
"description": "Agentes importados mediante un enlace público",
|
||||
"emptyState": "No se encontraron agentes compartidos."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"headings": {
|
||||
"new": "Nuevo Agente",
|
||||
"edit": "Editar Agente",
|
||||
"draft": "Nuevo Agente (Borrador)"
|
||||
},
|
||||
"buttons": {
|
||||
"publish": "Publicar",
|
||||
"save": "Guardar",
|
||||
"saveDraft": "Guardar Borrador",
|
||||
"cancel": "Cancelar",
|
||||
"delete": "Eliminar",
|
||||
"logs": "Registros",
|
||||
"accessDetails": "Detalles de Acceso",
|
||||
"add": "Agregar"
|
||||
},
|
||||
"sections": {
|
||||
"meta": "Meta",
|
||||
"source": "Fuente",
|
||||
"prompt": "Prompt",
|
||||
"tools": "Herramientas",
|
||||
"agentType": "Tipo de agente",
|
||||
"advanced": "Avanzado",
|
||||
"preview": "Vista previa"
|
||||
},
|
||||
"placeholders": {
|
||||
"agentName": "Nombre del agente",
|
||||
"describeAgent": "Describe tu agente",
|
||||
"selectSources": "Seleccionar fuentes",
|
||||
"chunksPerQuery": "Fragmentos por consulta",
|
||||
"selectType": "Seleccionar tipo",
|
||||
"selectTools": "Seleccionar herramientas",
|
||||
"enterTokenLimit": "Ingresar límite de tokens",
|
||||
"enterRequestLimit": "Ingresar límite de solicitudes"
|
||||
},
|
||||
"sourcePopup": {
|
||||
"title": "Seleccionar Fuentes",
|
||||
"searchPlaceholder": "Buscar fuentes...",
|
||||
"noOptionsMessage": "No hay fuentes disponibles"
|
||||
},
|
||||
"toolsPopup": {
|
||||
"title": "Seleccionar Herramientas",
|
||||
"searchPlaceholder": "Buscar herramientas...",
|
||||
"noOptionsMessage": "No hay herramientas disponibles"
|
||||
},
|
||||
"upload": {
|
||||
"clickToUpload": "Haz clic para subir",
|
||||
"dragAndDrop": " o arrastra y suelta"
|
||||
},
|
||||
"agentTypes": {
|
||||
"classic": "Clásico",
|
||||
"react": "ReAct"
|
||||
},
|
||||
"advanced": {
|
||||
"jsonSchema": "Esquema de respuesta JSON",
|
||||
"jsonSchemaDescription": "Define un esquema JSON para aplicar formato de salida estructurado",
|
||||
"validJson": "JSON válido",
|
||||
"invalidJson": "JSON inválido - corrige para habilitar el guardado",
|
||||
"tokenLimiting": "Límite de tokens",
|
||||
"tokenLimitingDescription": "Limita el total diario de tokens que puede usar este agente",
|
||||
"requestLimiting": "Límite de solicitudes",
|
||||
"requestLimitingDescription": "Limita el total diario de solicitudes que se pueden hacer a este agente"
|
||||
},
|
||||
"preview": {
|
||||
"publishedPreview": "Los agentes publicados se pueden previsualizar aquí"
|
||||
},
|
||||
"externalKb": "KB Externa"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Registros del Agente",
|
||||
"lastUsedAt": "Último uso",
|
||||
"noUsageHistory": "Sin historial de uso",
|
||||
"tableHeader": "Registros del endpoint del agente"
|
||||
},
|
||||
"shared": {
|
||||
"notFound": "No se encontró el agente. Asegúrate de que el agente esté compartido."
|
||||
},
|
||||
"preview": {
|
||||
"testMessage": "Prueba tu agente aquí. Los agentes publicados se pueden usar en conversaciones."
|
||||
},
|
||||
"deleteConfirmation": "¿Estás seguro de que quieres eliminar este agente?"
|
||||
},
|
||||
"components": {
|
||||
"fileUpload": {
|
||||
"clickToUpload": "Click to upload or drag and drop",
|
||||
"dropFiles": "Drop the files here",
|
||||
"fileTypes": "PNG, JPG, JPEG up to",
|
||||
"sizeLimitUnit": "MB",
|
||||
"fileSizeError": "File exceeds {{size}}MB limit"
|
||||
}
|
||||
},
|
||||
"pageNotFound": {
|
||||
"title": "404",
|
||||
"message": "The page you are looking for does not exist.",
|
||||
"goHome": "Go Back Home"
|
||||
},
|
||||
"filePicker": {
|
||||
"searchPlaceholder": "Buscar archivos y carpetas...",
|
||||
"itemsSelected": "{{count}} seleccionados",
|
||||
"name": "Nombre",
|
||||
"lastModified": "Última modificación",
|
||||
"size": "Tamaño"
|
||||
},
|
||||
"actionButtons": {
|
||||
"openNewChat": "Abrir nuevo chat",
|
||||
"share": "Compartir"
|
||||
},
|
||||
"mermaid": {
|
||||
"downloadOptions": "Opciones de descarga",
|
||||
"viewCode": "Ver código",
|
||||
"decreaseZoom": "Reducir zoom",
|
||||
"resetZoom": "Restablecer zoom",
|
||||
"increaseZoom": "Aumentar zoom"
|
||||
},
|
||||
"navigation": {
|
||||
"agents": "Agentes"
|
||||
},
|
||||
"notification": {
|
||||
"ariaLabel": "Notificación",
|
||||
"closeAriaLabel": "Cerrar notificación"
|
||||
},
|
||||
"prompts": {
|
||||
"textAriaLabel": "Texto del prompt"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,8 +185,58 @@
|
||||
"cancel": "キャンセル",
|
||||
"addNew": "新規追加",
|
||||
"name": "名前",
|
||||
"type": "タイプ"
|
||||
}
|
||||
"type": "タイプ",
|
||||
"mcp": {
|
||||
"addServer": "Add MCP Server",
|
||||
"editServer": "Edit Server",
|
||||
"serverName": "Server Name",
|
||||
"serverUrl": "Server URL",
|
||||
"headerName": "Header Name",
|
||||
"timeout": "Timeout (seconds)",
|
||||
"testConnection": "Test Connection",
|
||||
"testing": "Testing",
|
||||
"saving": "Saving",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"noAuth": "No Authentication",
|
||||
"oauthInProgress": "Waiting for OAuth completion...",
|
||||
"oauthCompleted": "OAuth completed successfully",
|
||||
"authType": "Authentication Type",
|
||||
"defaultServerName": "My MCP Server",
|
||||
"authTypes": {
|
||||
"none": "No Authentication",
|
||||
"apiKey": "API Key",
|
||||
"bearer": "Bearer Token",
|
||||
"oauth": "OAuth",
|
||||
"basic": "Basic Authentication"
|
||||
},
|
||||
"placeholders": {
|
||||
"serverUrl": "https://api.example.com",
|
||||
"apiKey": "Your secret API key",
|
||||
"bearerToken": "Your secret token",
|
||||
"username": "Your username",
|
||||
"password": "Your password",
|
||||
"oauthScopes": "OAuth scopes (comma separated)"
|
||||
},
|
||||
"errors": {
|
||||
"nameRequired": "Server name is required",
|
||||
"urlRequired": "Server URL is required",
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"apiKeyRequired": "API key is required",
|
||||
"tokenRequired": "Bearer token is required",
|
||||
"usernameRequired": "Username is required",
|
||||
"passwordRequired": "Password is required",
|
||||
"testFailed": "Connection test failed",
|
||||
"saveFailed": "Failed to save MCP server",
|
||||
"oauthFailed": "OAuth process failed or was cancelled",
|
||||
"oauthTimeout": "OAuth process timed out, please try again",
|
||||
"timeoutRange": "Timeout must be between 1 and 300 seconds"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scrollTabsLeft": "タブを左にスクロール",
|
||||
"tabsAriaLabel": "設定タブ",
|
||||
"scrollTabsRight": "タブを右にスクロール"
|
||||
},
|
||||
"modals": {
|
||||
"uploadDoc": {
|
||||
@@ -306,7 +356,8 @@
|
||||
"disclaimer": "キーが表示されるのはこのときだけです。",
|
||||
"copy": "コピー",
|
||||
"copied": "コピーしました",
|
||||
"confirm": "キーを保存しました"
|
||||
"confirm": "キーを保存しました",
|
||||
"apiKeyLabel": "API Key"
|
||||
},
|
||||
"deleteConv": {
|
||||
"confirm": "すべての会話を削除してもよろしいですか?",
|
||||
@@ -324,18 +375,43 @@
|
||||
"apiKeyLabel": "APIキー / OAuth",
|
||||
"apiKeyPlaceholder": "APIキー / OAuthを入力してください",
|
||||
"addButton": "ツールを追加",
|
||||
"closeButton": "閉じる"
|
||||
"closeButton": "閉じる",
|
||||
"customNamePlaceholder": "Enter custom name (optional)"
|
||||
},
|
||||
"prompts": {
|
||||
"addPrompt": "プロンプトを追加",
|
||||
"addDescription": "カスタムプロンプトを追加してDocsGPTに保存",
|
||||
"addDescription": "カスタムプロンプトを追加して DocsGPT に保存します",
|
||||
"editPrompt": "プロンプトを編集",
|
||||
"editDescription": "カスタムプロンプトを編集してDocsGPTに保存",
|
||||
"editDescription": "カスタムプロンプトを編集して DocsGPT に保存します",
|
||||
"promptName": "プロンプト名",
|
||||
"promptText": "プロンプトテキスト",
|
||||
"promptText": "プロンプトのテキスト",
|
||||
"save": "保存",
|
||||
"nameExists": "名前が既に存在します",
|
||||
"deleteConfirmation": "プロンプト「{{name}}」を削除してもよろしいですか?"
|
||||
"cancel": "キャンセル",
|
||||
"nameExists": "この名前はすでに存在します",
|
||||
"deleteConfirmation": "プロンプト『{{name}}』を削除してもよろしいですか?",
|
||||
"placeholderText": "ここにプロンプトのテキストを入力してください…",
|
||||
"addExamplePlaceholder": "このテキストを要約してください:",
|
||||
"variablesLabel": "変数",
|
||||
"variablesSubtext": "クリックしてプロンプトに挿入",
|
||||
"variablesDescription": "クリックしてプロンプトに挿入",
|
||||
"systemVariables": "システム変数",
|
||||
"toolVariables": "ツール変数",
|
||||
"systemVariablesDropdownLabel": "System Variables",
|
||||
"systemVariableOptions": {
|
||||
"sourceContent": "Sources content",
|
||||
"sourceSummaries": "Alias for content (backward compatible)",
|
||||
"sourceDocuments": "Document objects list",
|
||||
"sourceCount": "Number of retrieved documents",
|
||||
"systemDate": "Current date (YYYY-MM-DD)",
|
||||
"systemTime": "Current time (HH:MM:SS)",
|
||||
"systemTimestamp": "ISO 8601 timestamp",
|
||||
"systemRequestId": "Unique request identifier",
|
||||
"systemUserId": "Current user ID"
|
||||
},
|
||||
"learnAboutPrompts": "プロンプトについて学ぶ →",
|
||||
"publicPromptEditDisabled": "公開プロンプトは編集できません",
|
||||
"promptTypePublic": "公開",
|
||||
"promptTypePrivate": "非公開"
|
||||
},
|
||||
"chunk": {
|
||||
"add": "チャンクを追加",
|
||||
@@ -349,6 +425,22 @@
|
||||
"cancel": "キャンセル",
|
||||
"delete": "削除",
|
||||
"deleteConfirmation": "このチャンクを削除してもよろしいですか?"
|
||||
},
|
||||
"addAction": {
|
||||
"title": "New Action",
|
||||
"actionNamePlaceholder": "Action Name",
|
||||
"invalidFormat": "Invalid function name format. Use only letters, numbers, underscores, and hyphens.",
|
||||
"formatHelp": "Use only letters, numbers, underscores, and hyphens (e.g., `get_data`, `send_report`, etc.)",
|
||||
"addButton": "Add"
|
||||
},
|
||||
"agentDetails": {
|
||||
"title": "Access Details",
|
||||
"publicLink": "Public Link",
|
||||
"apiKey": "API Key",
|
||||
"webhookUrl": "Webhook URL",
|
||||
"generate": "Generate",
|
||||
"test": "Test",
|
||||
"learnMore": "Learn more"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
@@ -391,6 +483,153 @@
|
||||
"attach": "添付",
|
||||
"remove": "添付ファイルを削除"
|
||||
},
|
||||
"retry": "再試行"
|
||||
"retry": "再試行",
|
||||
"reasoning": "推論"
|
||||
},
|
||||
"agents": {
|
||||
"title": "エージェント",
|
||||
"description": "指示、追加知識、スキルの組み合わせを含むDocsGPTのカスタムバージョンを発見して作成します",
|
||||
"newAgent": "新しいエージェント",
|
||||
"backToAll": "すべてのエージェントに戻る",
|
||||
"sections": {
|
||||
"template": {
|
||||
"title": "DocsGPT提供",
|
||||
"description": "DocsGPTが提供するエージェント",
|
||||
"emptyState": "テンプレートエージェントが見つかりません。"
|
||||
},
|
||||
"user": {
|
||||
"title": "自分のエージェント",
|
||||
"description": "あなたが作成または公開したエージェント",
|
||||
"emptyState": "まだ作成されたエージェントがありません。"
|
||||
},
|
||||
"shared": {
|
||||
"title": "共有されたエージェント",
|
||||
"description": "公開リンクを使用してインポートされたエージェント",
|
||||
"emptyState": "共有エージェントが見つかりません。"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"headings": {
|
||||
"new": "新しいエージェント",
|
||||
"edit": "エージェントを編集",
|
||||
"draft": "新しいエージェント(下書き)"
|
||||
},
|
||||
"buttons": {
|
||||
"publish": "公開",
|
||||
"save": "保存",
|
||||
"saveDraft": "下書きを保存",
|
||||
"cancel": "キャンセル",
|
||||
"delete": "削除",
|
||||
"logs": "ログ",
|
||||
"accessDetails": "アクセス詳細",
|
||||
"add": "追加"
|
||||
},
|
||||
"sections": {
|
||||
"meta": "メタ",
|
||||
"source": "ソース",
|
||||
"prompt": "プロンプト",
|
||||
"tools": "ツール",
|
||||
"agentType": "エージェントタイプ",
|
||||
"advanced": "詳細設定",
|
||||
"preview": "プレビュー"
|
||||
},
|
||||
"placeholders": {
|
||||
"agentName": "エージェント名",
|
||||
"describeAgent": "エージェントを説明してください",
|
||||
"selectSources": "ソースを選択",
|
||||
"chunksPerQuery": "クエリごとのチャンク数",
|
||||
"selectType": "タイプを選択",
|
||||
"selectTools": "ツールを選択",
|
||||
"enterTokenLimit": "トークン制限を入力",
|
||||
"enterRequestLimit": "リクエスト制限を入力"
|
||||
},
|
||||
"sourcePopup": {
|
||||
"title": "ソースを選択",
|
||||
"searchPlaceholder": "ソースを検索...",
|
||||
"noOptionsMessage": "利用可能なソースがありません"
|
||||
},
|
||||
"toolsPopup": {
|
||||
"title": "ツールを選択",
|
||||
"searchPlaceholder": "ツールを検索...",
|
||||
"noOptionsMessage": "利用可能なツールがありません"
|
||||
},
|
||||
"upload": {
|
||||
"clickToUpload": "クリックしてアップロード",
|
||||
"dragAndDrop": " またはドラッグ&ドロップ"
|
||||
},
|
||||
"agentTypes": {
|
||||
"classic": "クラシック",
|
||||
"react": "ReAct"
|
||||
},
|
||||
"advanced": {
|
||||
"jsonSchema": "JSON応答スキーマ",
|
||||
"jsonSchemaDescription": "構造化された出力形式を適用するためのJSONスキーマを定義します",
|
||||
"validJson": "有効なJSON",
|
||||
"invalidJson": "無効なJSON - 保存を有効にするには修正してください",
|
||||
"tokenLimiting": "トークン制限",
|
||||
"tokenLimitingDescription": "このエージェントが使用できる1日の合計トークン数を制限します",
|
||||
"requestLimiting": "リクエスト制限",
|
||||
"requestLimitingDescription": "このエージェントに対して行える1日の合計リクエスト数を制限します"
|
||||
},
|
||||
"preview": {
|
||||
"publishedPreview": "公開されたエージェントはここでプレビューできます"
|
||||
},
|
||||
"externalKb": "外部KB"
|
||||
},
|
||||
"logs": {
|
||||
"title": "エージェントログ",
|
||||
"lastUsedAt": "最終使用日時",
|
||||
"noUsageHistory": "使用履歴がありません",
|
||||
"tableHeader": "エージェントエンドポイントログ"
|
||||
},
|
||||
"shared": {
|
||||
"notFound": "エージェントが見つかりません。エージェントが共有されていることを確認してください。"
|
||||
},
|
||||
"preview": {
|
||||
"testMessage": "ここでエージェントをテストできます。公開されたエージェントは会話で使用できます。"
|
||||
},
|
||||
"deleteConfirmation": "このエージェントを削除してもよろしいですか?"
|
||||
},
|
||||
"components": {
|
||||
"fileUpload": {
|
||||
"clickToUpload": "Click to upload or drag and drop",
|
||||
"dropFiles": "Drop the files here",
|
||||
"fileTypes": "PNG, JPG, JPEG up to",
|
||||
"sizeLimitUnit": "MB",
|
||||
"fileSizeError": "File exceeds {{size}}MB limit"
|
||||
}
|
||||
},
|
||||
"pageNotFound": {
|
||||
"title": "404",
|
||||
"message": "The page you are looking for does not exist.",
|
||||
"goHome": "Go Back Home"
|
||||
},
|
||||
"filePicker": {
|
||||
"searchPlaceholder": "ファイルとフォルダを検索...",
|
||||
"itemsSelected": "{{count}} 件選択済み",
|
||||
"name": "名前",
|
||||
"lastModified": "最終更新日",
|
||||
"size": "サイズ"
|
||||
},
|
||||
"actionButtons": {
|
||||
"openNewChat": "新しいチャットを開く",
|
||||
"share": "共有"
|
||||
},
|
||||
"mermaid": {
|
||||
"downloadOptions": "ダウンロードオプション",
|
||||
"viewCode": "コードを表示",
|
||||
"decreaseZoom": "ズームアウト",
|
||||
"resetZoom": "ズームをリセット",
|
||||
"increaseZoom": "ズームイン"
|
||||
},
|
||||
"navigation": {
|
||||
"agents": "エージェント"
|
||||
},
|
||||
"notification": {
|
||||
"ariaLabel": "通知",
|
||||
"closeAriaLabel": "通知を閉じる"
|
||||
},
|
||||
"prompts": {
|
||||
"textAriaLabel": "プロンプトテキスト"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,8 +185,58 @@
|
||||
"cancel": "Отмена",
|
||||
"addNew": "Добавить новое",
|
||||
"name": "Имя",
|
||||
"type": "Тип"
|
||||
}
|
||||
"type": "Тип",
|
||||
"mcp": {
|
||||
"addServer": "Add MCP Server",
|
||||
"editServer": "Edit Server",
|
||||
"serverName": "Server Name",
|
||||
"serverUrl": "Server URL",
|
||||
"headerName": "Header Name",
|
||||
"timeout": "Timeout (seconds)",
|
||||
"testConnection": "Test Connection",
|
||||
"testing": "Testing",
|
||||
"saving": "Saving",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"noAuth": "No Authentication",
|
||||
"oauthInProgress": "Waiting for OAuth completion...",
|
||||
"oauthCompleted": "OAuth completed successfully",
|
||||
"authType": "Authentication Type",
|
||||
"defaultServerName": "My MCP Server",
|
||||
"authTypes": {
|
||||
"none": "No Authentication",
|
||||
"apiKey": "API Key",
|
||||
"bearer": "Bearer Token",
|
||||
"oauth": "OAuth",
|
||||
"basic": "Basic Authentication"
|
||||
},
|
||||
"placeholders": {
|
||||
"serverUrl": "https://api.example.com",
|
||||
"apiKey": "Your secret API key",
|
||||
"bearerToken": "Your secret token",
|
||||
"username": "Your username",
|
||||
"password": "Your password",
|
||||
"oauthScopes": "OAuth scopes (comma separated)"
|
||||
},
|
||||
"errors": {
|
||||
"nameRequired": "Server name is required",
|
||||
"urlRequired": "Server URL is required",
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"apiKeyRequired": "API key is required",
|
||||
"tokenRequired": "Bearer token is required",
|
||||
"usernameRequired": "Username is required",
|
||||
"passwordRequired": "Password is required",
|
||||
"testFailed": "Connection test failed",
|
||||
"saveFailed": "Failed to save MCP server",
|
||||
"oauthFailed": "OAuth process failed or was cancelled",
|
||||
"oauthTimeout": "OAuth process timed out, please try again",
|
||||
"timeoutRange": "Timeout must be between 1 and 300 seconds"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scrollTabsLeft": "Прокрутить вкладки влево",
|
||||
"tabsAriaLabel": "Вкладки настроек",
|
||||
"scrollTabsRight": "Прокрутить вкладки вправо"
|
||||
},
|
||||
"modals": {
|
||||
"uploadDoc": {
|
||||
@@ -306,7 +356,8 @@
|
||||
"disclaimer": "Ваш ключ будет показан только один раз.",
|
||||
"copy": "Копировать",
|
||||
"copied": "Скопировано",
|
||||
"confirm": "Я сохранил ключ"
|
||||
"confirm": "Я сохранил ключ",
|
||||
"apiKeyLabel": "API Key"
|
||||
},
|
||||
"deleteConv": {
|
||||
"confirm": "Вы уверены, что хотите удалить все разговоры?",
|
||||
@@ -324,18 +375,43 @@
|
||||
"apiKeyLabel": "API ключ / OAuth",
|
||||
"apiKeyPlaceholder": "Введите API ключ / OAuth",
|
||||
"addButton": "Добавить инструмент",
|
||||
"closeButton": "Закрыть"
|
||||
"closeButton": "Закрыть",
|
||||
"customNamePlaceholder": "Enter custom name (optional)"
|
||||
},
|
||||
"prompts": {
|
||||
"addPrompt": "Добавить подсказку",
|
||||
"addDescription": "Добавить вашу пользовательскую подсказку и сохранить её в DocsGPT",
|
||||
"editPrompt": "Редактировать подсказку",
|
||||
"editDescription": "Редактировать вашу пользовательскую подсказку и сохранить её в DocsGPT",
|
||||
"promptName": "Название подсказки",
|
||||
"promptText": "Текст подсказки",
|
||||
"addPrompt": "Добавить промпт",
|
||||
"addDescription": "Добавьте свой собственный промпт и сохраните его в DocsGPT",
|
||||
"editPrompt": "Редактировать промпт",
|
||||
"editDescription": "Отредактируйте свой промпт и сохраните его в DocsGPT",
|
||||
"promptName": "Название промпта",
|
||||
"promptText": "Текст промпта",
|
||||
"save": "Сохранить",
|
||||
"nameExists": "Название уже существует",
|
||||
"deleteConfirmation": "Вы уверены, что хотите удалить подсказку «{{name}}»?"
|
||||
"cancel": "Отмена",
|
||||
"nameExists": "Имя уже существует",
|
||||
"deleteConfirmation": "Вы уверены, что хотите удалить промпт «{{name}}»?",
|
||||
"placeholderText": "Введите текст вашего промпта здесь...",
|
||||
"addExamplePlaceholder": "Пожалуйста, кратко изложите этот текст:",
|
||||
"variablesLabel": "Переменные",
|
||||
"variablesSubtext": "Нажмите, чтобы вставить в промпт",
|
||||
"variablesDescription": "Нажмите, чтобы вставить в промпт",
|
||||
"systemVariables": "Системные переменные",
|
||||
"toolVariables": "Переменные инструментов",
|
||||
"systemVariablesDropdownLabel": "Системные переменные",
|
||||
"systemVariableOptions": {
|
||||
"sourceContent": "Содержимое источников",
|
||||
"sourceSummaries": "Псевдоним содержимого (обратная совместимость)",
|
||||
"sourceDocuments": "Список объектов документов",
|
||||
"sourceCount": "Количество полученных документов",
|
||||
"systemDate": "Текущая дата (ГГГГ-ММ-ДД)",
|
||||
"systemTime": "Текущее время (ЧЧ:ММ:СС)",
|
||||
"systemTimestamp": "Отметка времени ISO 8601",
|
||||
"systemRequestId": "Уникальный идентификатор запроса",
|
||||
"systemUserId": "Идентификатор текущего пользователя"
|
||||
},
|
||||
"learnAboutPrompts": "Узнать о промптах →",
|
||||
"publicPromptEditDisabled": "Публичные промпты нельзя редактировать",
|
||||
"promptTypePublic": "публичный",
|
||||
"promptTypePrivate": "приватный"
|
||||
},
|
||||
"chunk": {
|
||||
"add": "Добавить фрагмент",
|
||||
@@ -349,6 +425,22 @@
|
||||
"cancel": "Отмена",
|
||||
"delete": "Удалить",
|
||||
"deleteConfirmation": "Вы уверены, что хотите удалить этот фрагмент?"
|
||||
},
|
||||
"addAction": {
|
||||
"title": "New Action",
|
||||
"actionNamePlaceholder": "Action Name",
|
||||
"invalidFormat": "Invalid function name format. Use only letters, numbers, underscores, and hyphens.",
|
||||
"formatHelp": "Use only letters, numbers, underscores, and hyphens (e.g., `get_data`, `send_report`, etc.)",
|
||||
"addButton": "Add"
|
||||
},
|
||||
"agentDetails": {
|
||||
"title": "Access Details",
|
||||
"publicLink": "Public Link",
|
||||
"apiKey": "API Key",
|
||||
"webhookUrl": "Webhook URL",
|
||||
"generate": "Generate",
|
||||
"test": "Test",
|
||||
"learnMore": "Learn more"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
@@ -391,6 +483,153 @@
|
||||
"attach": "Прикрепить",
|
||||
"remove": "Удалить вложение"
|
||||
},
|
||||
"retry": "Повторить"
|
||||
"retry": "Повторить",
|
||||
"reasoning": "Рассуждение"
|
||||
},
|
||||
"agents": {
|
||||
"title": "Агенты",
|
||||
"description": "Откройте и создайте пользовательские версии DocsGPT, которые объединяют инструкции, дополнительные знания и любую комбинацию навыков",
|
||||
"newAgent": "Новый Агент",
|
||||
"backToAll": "Вернуться ко всем агентам",
|
||||
"sections": {
|
||||
"template": {
|
||||
"title": "От DocsGPT",
|
||||
"description": "Агенты, предоставленные DocsGPT",
|
||||
"emptyState": "Шаблонные агенты не найдены."
|
||||
},
|
||||
"user": {
|
||||
"title": "Мои",
|
||||
"description": "Агенты, созданные или опубликованные вами",
|
||||
"emptyState": "У вас пока нет созданных агентов."
|
||||
},
|
||||
"shared": {
|
||||
"title": "Поделились со мной",
|
||||
"description": "Агенты, импортированные по публичной ссылке",
|
||||
"emptyState": "Общие агенты не найдены."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"headings": {
|
||||
"new": "Новый Агент",
|
||||
"edit": "Редактировать Агента",
|
||||
"draft": "Новый Агент (Черновик)"
|
||||
},
|
||||
"buttons": {
|
||||
"publish": "Опубликовать",
|
||||
"save": "Сохранить",
|
||||
"saveDraft": "Сохранить Черновик",
|
||||
"cancel": "Отмена",
|
||||
"delete": "Удалить",
|
||||
"logs": "Логи",
|
||||
"accessDetails": "Детали Доступа",
|
||||
"add": "Добавить"
|
||||
},
|
||||
"sections": {
|
||||
"meta": "Мета",
|
||||
"source": "Источник",
|
||||
"prompt": "Промпт",
|
||||
"tools": "Инструменты",
|
||||
"agentType": "Тип агента",
|
||||
"advanced": "Расширенные",
|
||||
"preview": "Предпросмотр"
|
||||
},
|
||||
"placeholders": {
|
||||
"agentName": "Имя агента",
|
||||
"describeAgent": "Опишите вашего агента",
|
||||
"selectSources": "Выберите источники",
|
||||
"chunksPerQuery": "Фрагментов на запрос",
|
||||
"selectType": "Выберите тип",
|
||||
"selectTools": "Выберите инструменты",
|
||||
"enterTokenLimit": "Введите лимит токенов",
|
||||
"enterRequestLimit": "Введите лимит запросов"
|
||||
},
|
||||
"sourcePopup": {
|
||||
"title": "Выберите Источники",
|
||||
"searchPlaceholder": "Поиск источников...",
|
||||
"noOptionsMessage": "Нет доступных источников"
|
||||
},
|
||||
"toolsPopup": {
|
||||
"title": "Выберите Инструменты",
|
||||
"searchPlaceholder": "Поиск инструментов...",
|
||||
"noOptionsMessage": "Нет доступных инструментов"
|
||||
},
|
||||
"upload": {
|
||||
"clickToUpload": "Нажмите для загрузки",
|
||||
"dragAndDrop": " или перетащите"
|
||||
},
|
||||
"agentTypes": {
|
||||
"classic": "Классический",
|
||||
"react": "ReAct"
|
||||
},
|
||||
"advanced": {
|
||||
"jsonSchema": "Схема ответа JSON",
|
||||
"jsonSchemaDescription": "Определите схему JSON для применения структурированного формата вывода",
|
||||
"validJson": "Валидный JSON",
|
||||
"invalidJson": "Невалидный JSON - исправьте для сохранения",
|
||||
"tokenLimiting": "Лимит токенов",
|
||||
"tokenLimitingDescription": "Ограничить ежедневное общее количество токенов, которые может использовать этот агент",
|
||||
"requestLimiting": "Лимит запросов",
|
||||
"requestLimitingDescription": "Ограничить ежедневное общее количество запросов, которые можно сделать к этому агенту"
|
||||
},
|
||||
"preview": {
|
||||
"publishedPreview": "Опубликованные агенты можно просмотреть здесь"
|
||||
},
|
||||
"externalKb": "Внешняя БЗ"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Логи Агента",
|
||||
"lastUsedAt": "Последнее использование",
|
||||
"noUsageHistory": "Нет истории использования",
|
||||
"tableHeader": "Логи конечной точки агента"
|
||||
},
|
||||
"shared": {
|
||||
"notFound": "Агент не найден. Убедитесь, что агент является общим."
|
||||
},
|
||||
"preview": {
|
||||
"testMessage": "Протестируйте своего агента здесь. Опубликованные агенты можно использовать в разговорах."
|
||||
},
|
||||
"deleteConfirmation": "Вы уверены, что хотите удалить этого агента?"
|
||||
},
|
||||
"components": {
|
||||
"fileUpload": {
|
||||
"clickToUpload": "Click to upload or drag and drop",
|
||||
"dropFiles": "Drop the files here",
|
||||
"fileTypes": "PNG, JPG, JPEG up to",
|
||||
"sizeLimitUnit": "MB",
|
||||
"fileSizeError": "File exceeds {{size}}MB limit"
|
||||
}
|
||||
},
|
||||
"pageNotFound": {
|
||||
"title": "404",
|
||||
"message": "The page you are looking for does not exist.",
|
||||
"goHome": "Go Back Home"
|
||||
},
|
||||
"filePicker": {
|
||||
"searchPlaceholder": "Поиск файлов и папок...",
|
||||
"itemsSelected": "{{count}} выбрано",
|
||||
"name": "Имя",
|
||||
"lastModified": "Последнее изменение",
|
||||
"size": "Размер"
|
||||
},
|
||||
"actionButtons": {
|
||||
"openNewChat": "Открыть новый чат",
|
||||
"share": "Поделиться"
|
||||
},
|
||||
"mermaid": {
|
||||
"downloadOptions": "Параметры загрузки",
|
||||
"viewCode": "Просмотр кода",
|
||||
"decreaseZoom": "Уменьшить масштаб",
|
||||
"resetZoom": "Сбросить масштаб",
|
||||
"increaseZoom": "Увеличить масштаб"
|
||||
},
|
||||
"navigation": {
|
||||
"agents": "Агенты"
|
||||
},
|
||||
"notification": {
|
||||
"ariaLabel": "Уведомление",
|
||||
"closeAriaLabel": "Закрыть уведомление"
|
||||
},
|
||||
"prompts": {
|
||||
"textAriaLabel": "Текст подсказки"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,8 +185,58 @@
|
||||
"cancel": "取消",
|
||||
"addNew": "新增",
|
||||
"name": "名稱",
|
||||
"type": "類型"
|
||||
}
|
||||
"type": "類型",
|
||||
"mcp": {
|
||||
"addServer": "Add MCP Server",
|
||||
"editServer": "Edit Server",
|
||||
"serverName": "Server Name",
|
||||
"serverUrl": "Server URL",
|
||||
"headerName": "Header Name",
|
||||
"timeout": "Timeout (seconds)",
|
||||
"testConnection": "Test Connection",
|
||||
"testing": "Testing",
|
||||
"saving": "Saving",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"noAuth": "No Authentication",
|
||||
"oauthInProgress": "Waiting for OAuth completion...",
|
||||
"oauthCompleted": "OAuth completed successfully",
|
||||
"authType": "Authentication Type",
|
||||
"defaultServerName": "My MCP Server",
|
||||
"authTypes": {
|
||||
"none": "No Authentication",
|
||||
"apiKey": "API Key",
|
||||
"bearer": "Bearer Token",
|
||||
"oauth": "OAuth",
|
||||
"basic": "Basic Authentication"
|
||||
},
|
||||
"placeholders": {
|
||||
"serverUrl": "https://api.example.com",
|
||||
"apiKey": "Your secret API key",
|
||||
"bearerToken": "Your secret token",
|
||||
"username": "Your username",
|
||||
"password": "Your password",
|
||||
"oauthScopes": "OAuth scopes (comma separated)"
|
||||
},
|
||||
"errors": {
|
||||
"nameRequired": "Server name is required",
|
||||
"urlRequired": "Server URL is required",
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"apiKeyRequired": "API key is required",
|
||||
"tokenRequired": "Bearer token is required",
|
||||
"usernameRequired": "Username is required",
|
||||
"passwordRequired": "Password is required",
|
||||
"testFailed": "Connection test failed",
|
||||
"saveFailed": "Failed to save MCP server",
|
||||
"oauthFailed": "OAuth process failed or was cancelled",
|
||||
"oauthTimeout": "OAuth process timed out, please try again",
|
||||
"timeoutRange": "Timeout must be between 1 and 300 seconds"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scrollTabsLeft": "向左捲動標籤",
|
||||
"tabsAriaLabel": "設定標籤",
|
||||
"scrollTabsRight": "向右捲動標籤"
|
||||
},
|
||||
"modals": {
|
||||
"uploadDoc": {
|
||||
@@ -306,7 +356,8 @@
|
||||
"disclaimer": "這是唯一一次顯示您的金鑰。",
|
||||
"copy": "複製",
|
||||
"copied": "已複製",
|
||||
"confirm": "我已儲存金鑰"
|
||||
"confirm": "我已儲存金鑰",
|
||||
"apiKeyLabel": "API Key"
|
||||
},
|
||||
"deleteConv": {
|
||||
"confirm": "您確定要刪除所有對話嗎?",
|
||||
@@ -324,7 +375,8 @@
|
||||
"apiKeyLabel": "API 金鑰 / OAuth",
|
||||
"apiKeyPlaceholder": "輸入 API 金鑰 / OAuth",
|
||||
"addButton": "新增工具",
|
||||
"closeButton": "關閉"
|
||||
"closeButton": "關閉",
|
||||
"customNamePlaceholder": "Enter custom name (optional)"
|
||||
},
|
||||
"prompts": {
|
||||
"addPrompt": "新增提示",
|
||||
@@ -334,8 +386,32 @@
|
||||
"promptName": "提示名稱",
|
||||
"promptText": "提示文字",
|
||||
"save": "儲存",
|
||||
"cancel": "取消",
|
||||
"nameExists": "名稱已存在",
|
||||
"deleteConfirmation": "您確定要刪除提示「{{name}}」嗎?"
|
||||
"deleteConfirmation": "您確定要刪除提示「{{name}}」嗎?",
|
||||
"placeholderText": "在此輸入提示內容...",
|
||||
"addExamplePlaceholder": "請總結此文本:",
|
||||
"variablesLabel": "變數",
|
||||
"variablesSubtext": "點擊以插入提示中",
|
||||
"variablesDescription": "點擊以插入到提示中",
|
||||
"systemVariables": "點擊以插入提示中",
|
||||
"toolVariables": "工具變數",
|
||||
"systemVariablesDropdownLabel": "系統變數",
|
||||
"systemVariableOptions": {
|
||||
"sourceContent": "來源內容",
|
||||
"sourceSummaries": "內容別名(向後相容)",
|
||||
"sourceDocuments": "文件物件列表",
|
||||
"sourceCount": "擷取的文件數量",
|
||||
"systemDate": "目前日期 (YYYY-MM-DD)",
|
||||
"systemTime": "目前時間 (HH:MM:SS)",
|
||||
"systemTimestamp": "ISO 8601 時間戳記",
|
||||
"systemRequestId": "唯一請求識別碼",
|
||||
"systemUserId": "目前使用者 ID"
|
||||
},
|
||||
"learnAboutPrompts": "了解提示 →",
|
||||
"publicPromptEditDisabled": "公共提示無法編輯",
|
||||
"promptTypePublic": "公共",
|
||||
"promptTypePrivate": "私人"
|
||||
},
|
||||
"chunk": {
|
||||
"add": "新增區塊",
|
||||
@@ -349,6 +425,22 @@
|
||||
"cancel": "取消",
|
||||
"delete": "刪除",
|
||||
"deleteConfirmation": "您確定要刪除此區塊嗎?"
|
||||
},
|
||||
"addAction": {
|
||||
"title": "New Action",
|
||||
"actionNamePlaceholder": "Action Name",
|
||||
"invalidFormat": "Invalid function name format. Use only letters, numbers, underscores, and hyphens.",
|
||||
"formatHelp": "Use only letters, numbers, underscores, and hyphens (e.g., `get_data`, `send_report`, etc.)",
|
||||
"addButton": "Add"
|
||||
},
|
||||
"agentDetails": {
|
||||
"title": "Access Details",
|
||||
"publicLink": "Public Link",
|
||||
"apiKey": "API Key",
|
||||
"webhookUrl": "Webhook URL",
|
||||
"generate": "Generate",
|
||||
"test": "Test",
|
||||
"learnMore": "Learn more"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
@@ -391,6 +483,153 @@
|
||||
"attach": "附件",
|
||||
"remove": "刪除附件"
|
||||
},
|
||||
"retry": "重試"
|
||||
"retry": "重試",
|
||||
"reasoning": "推理"
|
||||
},
|
||||
"agents": {
|
||||
"title": "代理",
|
||||
"description": "探索並創建結合指令、額外知識和任意技能組合的DocsGPT自訂版本",
|
||||
"newAgent": "新建代理",
|
||||
"backToAll": "返回所有代理",
|
||||
"sections": {
|
||||
"template": {
|
||||
"title": "由DocsGPT提供",
|
||||
"description": "DocsGPT提供的代理",
|
||||
"emptyState": "未找到範本代理。"
|
||||
},
|
||||
"user": {
|
||||
"title": "我的代理",
|
||||
"description": "您創建或發佈的代理",
|
||||
"emptyState": "您還沒有創建任何代理。"
|
||||
},
|
||||
"shared": {
|
||||
"title": "與我共享",
|
||||
"description": "透過公共連結匯入的代理",
|
||||
"emptyState": "未找到共享代理。"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"headings": {
|
||||
"new": "新建代理",
|
||||
"edit": "編輯代理",
|
||||
"draft": "新建代理(草稿)"
|
||||
},
|
||||
"buttons": {
|
||||
"publish": "發佈",
|
||||
"save": "儲存",
|
||||
"saveDraft": "儲存草稿",
|
||||
"cancel": "取消",
|
||||
"delete": "刪除",
|
||||
"logs": "日誌",
|
||||
"accessDetails": "存取詳情",
|
||||
"add": "新增"
|
||||
},
|
||||
"sections": {
|
||||
"meta": "中繼資料",
|
||||
"source": "來源",
|
||||
"prompt": "提示詞",
|
||||
"tools": "工具",
|
||||
"agentType": "代理類型",
|
||||
"advanced": "進階",
|
||||
"preview": "預覽"
|
||||
},
|
||||
"placeholders": {
|
||||
"agentName": "代理名稱",
|
||||
"describeAgent": "描述您的代理",
|
||||
"selectSources": "選擇來源",
|
||||
"chunksPerQuery": "每次查詢的區塊數",
|
||||
"selectType": "選擇類型",
|
||||
"selectTools": "選擇工具",
|
||||
"enterTokenLimit": "輸入權杖限制",
|
||||
"enterRequestLimit": "輸入請求限制"
|
||||
},
|
||||
"sourcePopup": {
|
||||
"title": "選擇來源",
|
||||
"searchPlaceholder": "搜尋來源...",
|
||||
"noOptionsMessage": "沒有可用的來源"
|
||||
},
|
||||
"toolsPopup": {
|
||||
"title": "選擇工具",
|
||||
"searchPlaceholder": "搜尋工具...",
|
||||
"noOptionsMessage": "沒有可用的工具"
|
||||
},
|
||||
"upload": {
|
||||
"clickToUpload": "點擊上傳",
|
||||
"dragAndDrop": " 或拖放"
|
||||
},
|
||||
"agentTypes": {
|
||||
"classic": "經典",
|
||||
"react": "ReAct"
|
||||
},
|
||||
"advanced": {
|
||||
"jsonSchema": "JSON回應架構",
|
||||
"jsonSchemaDescription": "定義JSON架構以強制執行結構化輸出格式",
|
||||
"validJson": "有效的JSON",
|
||||
"invalidJson": "無效的JSON - 修復後才能儲存",
|
||||
"tokenLimiting": "權杖限制",
|
||||
"tokenLimitingDescription": "限制此代理每天可使用的總權杖數",
|
||||
"requestLimiting": "請求限制",
|
||||
"requestLimitingDescription": "限制每天可向此代理發出的總請求數"
|
||||
},
|
||||
"preview": {
|
||||
"publishedPreview": "已發佈的代理可以在此處預覽"
|
||||
},
|
||||
"externalKb": "外部知識庫"
|
||||
},
|
||||
"logs": {
|
||||
"title": "代理日誌",
|
||||
"lastUsedAt": "最後使用時間",
|
||||
"noUsageHistory": "無使用歷史",
|
||||
"tableHeader": "代理端點日誌"
|
||||
},
|
||||
"shared": {
|
||||
"notFound": "未找到代理。請確保代理已共享。"
|
||||
},
|
||||
"preview": {
|
||||
"testMessage": "在此測試您的代理。已發佈的代理可以在對話中使用。"
|
||||
},
|
||||
"deleteConfirmation": "您確定要刪除此代理嗎?"
|
||||
},
|
||||
"components": {
|
||||
"fileUpload": {
|
||||
"clickToUpload": "Click to upload or drag and drop",
|
||||
"dropFiles": "Drop the files here",
|
||||
"fileTypes": "PNG, JPG, JPEG up to",
|
||||
"sizeLimitUnit": "MB",
|
||||
"fileSizeError": "File exceeds {{size}}MB limit"
|
||||
}
|
||||
},
|
||||
"pageNotFound": {
|
||||
"title": "404",
|
||||
"message": "The page you are looking for does not exist.",
|
||||
"goHome": "Go Back Home"
|
||||
},
|
||||
"filePicker": {
|
||||
"searchPlaceholder": "搜尋檔案和資料夾...",
|
||||
"itemsSelected": "已選擇 {{count}} 項",
|
||||
"name": "名稱",
|
||||
"lastModified": "最後修改",
|
||||
"size": "大小"
|
||||
},
|
||||
"actionButtons": {
|
||||
"openNewChat": "開啟新聊天",
|
||||
"share": "分享"
|
||||
},
|
||||
"mermaid": {
|
||||
"downloadOptions": "下載選項",
|
||||
"viewCode": "查看程式碼",
|
||||
"decreaseZoom": "縮小",
|
||||
"resetZoom": "重設縮放",
|
||||
"increaseZoom": "放大"
|
||||
},
|
||||
"navigation": {
|
||||
"agents": "代理"
|
||||
},
|
||||
"notification": {
|
||||
"ariaLabel": "通知",
|
||||
"closeAriaLabel": "關閉通知"
|
||||
},
|
||||
"prompts": {
|
||||
"textAriaLabel": "提示文字"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,8 +185,58 @@
|
||||
"cancel": "取消",
|
||||
"addNew": "添加新的",
|
||||
"name": "名称",
|
||||
"type": "类型"
|
||||
}
|
||||
"type": "类型",
|
||||
"mcp": {
|
||||
"addServer": "Add MCP Server",
|
||||
"editServer": "Edit Server",
|
||||
"serverName": "Server Name",
|
||||
"serverUrl": "Server URL",
|
||||
"headerName": "Header Name",
|
||||
"timeout": "Timeout (seconds)",
|
||||
"testConnection": "Test Connection",
|
||||
"testing": "Testing",
|
||||
"saving": "Saving",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"noAuth": "No Authentication",
|
||||
"oauthInProgress": "Waiting for OAuth completion...",
|
||||
"oauthCompleted": "OAuth completed successfully",
|
||||
"authType": "Authentication Type",
|
||||
"defaultServerName": "My MCP Server",
|
||||
"authTypes": {
|
||||
"none": "No Authentication",
|
||||
"apiKey": "API Key",
|
||||
"bearer": "Bearer Token",
|
||||
"oauth": "OAuth",
|
||||
"basic": "Basic Authentication"
|
||||
},
|
||||
"placeholders": {
|
||||
"serverUrl": "https://api.example.com",
|
||||
"apiKey": "Your secret API key",
|
||||
"bearerToken": "Your secret token",
|
||||
"username": "Your username",
|
||||
"password": "Your password",
|
||||
"oauthScopes": "OAuth scopes (comma separated)"
|
||||
},
|
||||
"errors": {
|
||||
"nameRequired": "Server name is required",
|
||||
"urlRequired": "Server URL is required",
|
||||
"invalidUrl": "Please enter a valid URL",
|
||||
"apiKeyRequired": "API key is required",
|
||||
"tokenRequired": "Bearer token is required",
|
||||
"usernameRequired": "Username is required",
|
||||
"passwordRequired": "Password is required",
|
||||
"testFailed": "Connection test failed",
|
||||
"saveFailed": "Failed to save MCP server",
|
||||
"oauthFailed": "OAuth process failed or was cancelled",
|
||||
"oauthTimeout": "OAuth process timed out, please try again",
|
||||
"timeoutRange": "Timeout must be between 1 and 300 seconds"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scrollTabsLeft": "向左滚动标签",
|
||||
"tabsAriaLabel": "设置标签",
|
||||
"scrollTabsRight": "向右滚动标签"
|
||||
},
|
||||
"modals": {
|
||||
"uploadDoc": {
|
||||
@@ -306,7 +356,8 @@
|
||||
"disclaimer": "这是您的密钥唯一一次展示机会。",
|
||||
"copy": "复制",
|
||||
"copied": "已复制",
|
||||
"confirm": "我已保存密钥"
|
||||
"confirm": "我已保存密钥",
|
||||
"apiKeyLabel": "API Key"
|
||||
},
|
||||
"deleteConv": {
|
||||
"confirm": "您确定要删除所有对话吗?",
|
||||
@@ -324,7 +375,8 @@
|
||||
"apiKeyLabel": "API 密钥 / OAuth",
|
||||
"apiKeyPlaceholder": "输入 API 密钥 / OAuth",
|
||||
"addButton": "添加工具",
|
||||
"closeButton": "关闭"
|
||||
"closeButton": "关闭",
|
||||
"customNamePlaceholder": "Enter custom name (optional)"
|
||||
},
|
||||
"prompts": {
|
||||
"addPrompt": "添加提示",
|
||||
@@ -335,7 +387,31 @@
|
||||
"promptText": "提示文本",
|
||||
"save": "保存",
|
||||
"nameExists": "名称已存在",
|
||||
"deleteConfirmation": "您确定要删除提示'{{name}}'吗?"
|
||||
"deleteConfirmation": "您确定要删除提示'{{name}}'吗?",
|
||||
"cancel": "取消",
|
||||
"placeholderText": "在此輸入提示內容...",
|
||||
"addExamplePlaceholder": "請總結此文本:",
|
||||
"variablesLabel": "變數",
|
||||
"variablesSubtext": "點擊以插入提示中",
|
||||
"variablesDescription": "點擊以插入到提示中",
|
||||
"systemVariables": "點擊以插入提示中",
|
||||
"toolVariables": "工具變數",
|
||||
"systemVariablesDropdownLabel": "系統變數",
|
||||
"systemVariableOptions": {
|
||||
"sourceContent": "來源內容",
|
||||
"sourceSummaries": "內容別名(向後相容)",
|
||||
"sourceDocuments": "文件物件列表",
|
||||
"sourceCount": "擷取的文件數量",
|
||||
"systemDate": "目前日期 (YYYY-MM-DD)",
|
||||
"systemTime": "目前時間 (HH:MM:SS)",
|
||||
"systemTimestamp": "ISO 8601 時間戳記",
|
||||
"systemRequestId": "唯一請求識別碼",
|
||||
"systemUserId": "目前使用者 ID"
|
||||
},
|
||||
"learnAboutPrompts": "了解提示 →",
|
||||
"publicPromptEditDisabled": "公共提示無法編輯",
|
||||
"promptTypePublic": "公共",
|
||||
"promptTypePrivate": "私人"
|
||||
},
|
||||
"chunk": {
|
||||
"add": "添加块",
|
||||
@@ -349,6 +425,22 @@
|
||||
"cancel": "取消",
|
||||
"delete": "删除",
|
||||
"deleteConfirmation": "您确定要删除此块吗?"
|
||||
},
|
||||
"addAction": {
|
||||
"title": "New Action",
|
||||
"actionNamePlaceholder": "Action Name",
|
||||
"invalidFormat": "Invalid function name format. Use only letters, numbers, underscores, and hyphens.",
|
||||
"formatHelp": "Use only letters, numbers, underscores, and hyphens (e.g., `get_data`, `send_report`, etc.)",
|
||||
"addButton": "Add"
|
||||
},
|
||||
"agentDetails": {
|
||||
"title": "Access Details",
|
||||
"publicLink": "Public Link",
|
||||
"apiKey": "API Key",
|
||||
"webhookUrl": "Webhook URL",
|
||||
"generate": "Generate",
|
||||
"test": "Test",
|
||||
"learnMore": "Learn more"
|
||||
}
|
||||
},
|
||||
"sharedConv": {
|
||||
@@ -391,6 +483,153 @@
|
||||
"attach": "附件",
|
||||
"remove": "删除附件"
|
||||
},
|
||||
"retry": "重试"
|
||||
"retry": "重试",
|
||||
"reasoning": "推理"
|
||||
},
|
||||
"agents": {
|
||||
"title": "代理",
|
||||
"description": "发现并创建结合指令、额外知识和任意技能组合的DocsGPT自定义版本",
|
||||
"newAgent": "新建代理",
|
||||
"backToAll": "返回所有代理",
|
||||
"sections": {
|
||||
"template": {
|
||||
"title": "由DocsGPT提供",
|
||||
"description": "DocsGPT提供的代理",
|
||||
"emptyState": "未找到模板代理。"
|
||||
},
|
||||
"user": {
|
||||
"title": "我的代理",
|
||||
"description": "您创建或发布的代理",
|
||||
"emptyState": "您还没有创建任何代理。"
|
||||
},
|
||||
"shared": {
|
||||
"title": "与我共享",
|
||||
"description": "通过公共链接导入的代理",
|
||||
"emptyState": "未找到共享代理。"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"headings": {
|
||||
"new": "新建代理",
|
||||
"edit": "编辑代理",
|
||||
"draft": "新建代理(草稿)"
|
||||
},
|
||||
"buttons": {
|
||||
"publish": "发布",
|
||||
"save": "保存",
|
||||
"saveDraft": "保存草稿",
|
||||
"cancel": "取消",
|
||||
"delete": "删除",
|
||||
"logs": "日志",
|
||||
"accessDetails": "访问详情",
|
||||
"add": "添加"
|
||||
},
|
||||
"sections": {
|
||||
"meta": "元数据",
|
||||
"source": "来源",
|
||||
"prompt": "提示词",
|
||||
"tools": "工具",
|
||||
"agentType": "代理类型",
|
||||
"advanced": "高级",
|
||||
"preview": "预览"
|
||||
},
|
||||
"placeholders": {
|
||||
"agentName": "代理名称",
|
||||
"describeAgent": "描述您的代理",
|
||||
"selectSources": "选择来源",
|
||||
"chunksPerQuery": "每次查询的块数",
|
||||
"selectType": "选择类型",
|
||||
"selectTools": "选择工具",
|
||||
"enterTokenLimit": "输入令牌限制",
|
||||
"enterRequestLimit": "输入请求限制"
|
||||
},
|
||||
"sourcePopup": {
|
||||
"title": "选择来源",
|
||||
"searchPlaceholder": "搜索来源...",
|
||||
"noOptionsMessage": "没有可用的来源"
|
||||
},
|
||||
"toolsPopup": {
|
||||
"title": "选择工具",
|
||||
"searchPlaceholder": "搜索工具...",
|
||||
"noOptionsMessage": "没有可用的工具"
|
||||
},
|
||||
"upload": {
|
||||
"clickToUpload": "点击上传",
|
||||
"dragAndDrop": " 或拖放"
|
||||
},
|
||||
"agentTypes": {
|
||||
"classic": "经典",
|
||||
"react": "ReAct"
|
||||
},
|
||||
"advanced": {
|
||||
"jsonSchema": "JSON响应架构",
|
||||
"jsonSchemaDescription": "定义JSON架构以强制执行结构化输出格式",
|
||||
"validJson": "有效的JSON",
|
||||
"invalidJson": "无效的JSON - 修复后才能保存",
|
||||
"tokenLimiting": "令牌限制",
|
||||
"tokenLimitingDescription": "限制此代理每天可使用的总令牌数",
|
||||
"requestLimiting": "请求限制",
|
||||
"requestLimitingDescription": "限制每天可向此代理发出的总请求数"
|
||||
},
|
||||
"preview": {
|
||||
"publishedPreview": "已发布的代理可以在此处预览"
|
||||
},
|
||||
"externalKb": "外部知识库"
|
||||
},
|
||||
"logs": {
|
||||
"title": "代理日志",
|
||||
"lastUsedAt": "最后使用时间",
|
||||
"noUsageHistory": "无使用历史",
|
||||
"tableHeader": "代理端点日志"
|
||||
},
|
||||
"shared": {
|
||||
"notFound": "未找到代理。请确保代理已共享。"
|
||||
},
|
||||
"preview": {
|
||||
"testMessage": "在此测试您的代理。已发布的代理可以在对话中使用。"
|
||||
},
|
||||
"deleteConfirmation": "您确定要删除此代理吗?"
|
||||
},
|
||||
"components": {
|
||||
"fileUpload": {
|
||||
"clickToUpload": "Click to upload or drag and drop",
|
||||
"dropFiles": "Drop the files here",
|
||||
"fileTypes": "PNG, JPG, JPEG up to",
|
||||
"sizeLimitUnit": "MB",
|
||||
"fileSizeError": "File exceeds {{size}}MB limit"
|
||||
}
|
||||
},
|
||||
"pageNotFound": {
|
||||
"title": "404",
|
||||
"message": "The page you are looking for does not exist.",
|
||||
"goHome": "Go Back Home"
|
||||
},
|
||||
"filePicker": {
|
||||
"searchPlaceholder": "搜索文件和文件夹...",
|
||||
"itemsSelected": "已选择 {{count}} 项",
|
||||
"name": "名称",
|
||||
"lastModified": "最后修改",
|
||||
"size": "大小"
|
||||
},
|
||||
"actionButtons": {
|
||||
"openNewChat": "打开新聊天",
|
||||
"share": "分享"
|
||||
},
|
||||
"mermaid": {
|
||||
"downloadOptions": "下载选项",
|
||||
"viewCode": "查看代码",
|
||||
"decreaseZoom": "缩小",
|
||||
"resetZoom": "重置缩放",
|
||||
"increaseZoom": "放大"
|
||||
},
|
||||
"navigation": {
|
||||
"agents": "代理"
|
||||
},
|
||||
"notification": {
|
||||
"ariaLabel": "通知",
|
||||
"closeAriaLabel": "关闭通知"
|
||||
},
|
||||
"prompts": {
|
||||
"textAriaLabel": "提示文本"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function AddActionModal({
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-jet dark:text-bright-gray px-3 text-xl font-semibold">
|
||||
New Action
|
||||
{t('modals.addAction.title')}
|
||||
</h2>
|
||||
<div className="relative mt-6 px-3">
|
||||
<Input
|
||||
@@ -57,7 +57,7 @@ export default function AddActionModal({
|
||||
}}
|
||||
borderVariant="thin"
|
||||
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||
placeholder="Action Name"
|
||||
placeholder={t('modals.addAction.actionNamePlaceholder')}
|
||||
required={true}
|
||||
/>
|
||||
<p
|
||||
@@ -66,8 +66,8 @@ export default function AddActionModal({
|
||||
}`}
|
||||
>
|
||||
{functionNameError
|
||||
? 'Invalid function name format. Use only letters, numbers, underscores, and hyphens.'
|
||||
: 'Use only letters, numbers, underscores, and hyphens (e.g., `get_data`, `send_report`, etc.)'}
|
||||
? t('modals.addAction.invalidFormat')
|
||||
: t('modals.addAction.formatHelp')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-row-reverse gap-1 px-3">
|
||||
@@ -75,7 +75,7 @@ export default function AddActionModal({
|
||||
onClick={handleAddAction}
|
||||
className="bg-purple-30 hover:bg-violets-are-blue rounded-3xl px-5 py-2 text-sm text-white transition-all"
|
||||
>
|
||||
Add
|
||||
{t('modals.addAction.addButton')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { Agent } from '../agents/types';
|
||||
@@ -24,6 +25,7 @@ export default function AgentDetailsModal({
|
||||
modalState,
|
||||
setModalState,
|
||||
}: AgentDetailsModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const token = useSelector(selectToken);
|
||||
|
||||
const [sharedToken, setSharedToken] = useState<string | null>(
|
||||
@@ -86,13 +88,13 @@ export default function AgentDetailsModal({
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-jet dark:text-bright-gray text-xl font-semibold">
|
||||
Access Details
|
||||
{t('modals.agentDetails.title')}
|
||||
</h2>
|
||||
<div className="mt-8 flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-jet dark:text-bright-gray text-base font-semibold">
|
||||
Public Link
|
||||
{t('modals.agentDetails.publicLink')}
|
||||
</h2>
|
||||
</div>
|
||||
{sharedToken ? (
|
||||
@@ -117,7 +119,9 @@ export default function AgentDetailsModal({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="text-sm">Learn more</span>
|
||||
<span className="text-sm">
|
||||
{t('modals.agentDetails.learnMore')}
|
||||
</span>
|
||||
<img
|
||||
src="/src/assets/external-link.svg"
|
||||
alt="External link"
|
||||
@@ -133,14 +137,14 @@ export default function AgentDetailsModal({
|
||||
{loadingStates.publicLink ? (
|
||||
<Spinner size="small" color="#976af3" />
|
||||
) : (
|
||||
'Generate'
|
||||
t('modals.agentDetails.generate')
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="text-jet dark:text-bright-gray text-base font-semibold">
|
||||
API Key
|
||||
{t('modals.agentDetails.apiKey')}
|
||||
</h2>
|
||||
{apiKey ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -162,7 +166,7 @@ export default function AgentDetailsModal({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Test
|
||||
{t('modals.agentDetails.test')}
|
||||
<img
|
||||
src="/src/assets/external-link.svg"
|
||||
alt="External link"
|
||||
@@ -174,14 +178,14 @@ export default function AgentDetailsModal({
|
||||
</div>
|
||||
) : (
|
||||
<button className="border-purple-30 text-purple-30 hover:bg-purple-30 w-28 rounded-3xl border border-solid px-5 py-2 text-sm font-medium transition-colors hover:text-white">
|
||||
Generate
|
||||
{t('modals.agentDetails.generate')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-jet dark:text-bright-gray text-base font-semibold">
|
||||
Webhook URL
|
||||
{t('modals.agentDetails.webhookUrl')}
|
||||
</h2>
|
||||
</div>
|
||||
{webhookUrl ? (
|
||||
@@ -202,7 +206,9 @@ export default function AgentDetailsModal({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="text-sm">Learn more</span>
|
||||
<span className="text-sm">
|
||||
{t('modals.agentDetails.learnMore')}
|
||||
</span>
|
||||
<img
|
||||
src="/src/assets/external-link.svg"
|
||||
alt="External link"
|
||||
@@ -218,7 +224,7 @@ export default function AgentDetailsModal({
|
||||
{loadingStates.webhook ? (
|
||||
<Spinner size="small" color="#976af3" />
|
||||
) : (
|
||||
'Generate'
|
||||
t('modals.agentDetails.generate')
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function ConfigToolModal({
|
||||
value={customName}
|
||||
onChange={(e) => setCustomName(e.target.value)}
|
||||
borderVariant="thin"
|
||||
placeholder="Enter custom name (optional)"
|
||||
placeholder={t('modals.configTool.customNamePlaceholder')}
|
||||
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import Dropdown from '../components/Dropdown';
|
||||
import Input from '../components/Input';
|
||||
import { CreateAPIKeyModalProps, Doc } from '../models/misc';
|
||||
import { selectSourceDocs, selectToken } from '../preferences/preferenceSlice';
|
||||
import WrapperModal from './WrapperModal';
|
||||
|
||||
const embeddingsName =
|
||||
import.meta.env.VITE_EMBEDDINGS_NAME ||
|
||||
'huggingface_sentence-transformers/all-mpnet-base-v2';
|
||||
|
||||
export default function CreateAPIKeyModal({
|
||||
close,
|
||||
createAPIKey,
|
||||
}: CreateAPIKeyModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const token = useSelector(selectToken);
|
||||
const docs = useSelector(selectSourceDocs);
|
||||
|
||||
const [APIKeyName, setAPIKeyName] = React.useState<string>('');
|
||||
const [sourcePath, setSourcePath] = React.useState<{
|
||||
name: string;
|
||||
id: string;
|
||||
type: string;
|
||||
} | null>(null);
|
||||
const [prompt, setPrompt] = React.useState<{
|
||||
name: string;
|
||||
id: string;
|
||||
type: string;
|
||||
} | null>(null);
|
||||
const [activePrompts, setActivePrompts] = React.useState<
|
||||
{ name: string; id: string; type: string }[]
|
||||
>([]);
|
||||
const [chunk, setChunk] = React.useState<string>('2');
|
||||
const chunkOptions = ['0', '2', '4', '6', '8', '10'];
|
||||
|
||||
const extractDocPaths = () =>
|
||||
docs
|
||||
? docs
|
||||
.filter((doc) => doc.model === embeddingsName)
|
||||
.map((doc: Doc) => {
|
||||
if ('id' in doc) {
|
||||
return {
|
||||
name: doc.name,
|
||||
id: doc.id as string,
|
||||
type: 'local',
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: doc.name,
|
||||
id: doc.id ?? 'default',
|
||||
type: doc.type ?? 'default',
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleFetchPrompts = async () => {
|
||||
try {
|
||||
const response = await userService.getPrompts(token);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch prompts');
|
||||
}
|
||||
const promptsData = await response.json();
|
||||
setActivePrompts(promptsData);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
handleFetchPrompts();
|
||||
}, []);
|
||||
return (
|
||||
<WrapperModal close={close} className="p-4">
|
||||
<div className="mb-6">
|
||||
<span className="text-jet dark:text-bright-gray text-xl">
|
||||
{t('modals.createAPIKey.label')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative mt-5 mb-4">
|
||||
<Input
|
||||
type="text"
|
||||
className="rounded-md"
|
||||
value={APIKeyName}
|
||||
placeholder={t('modals.createAPIKey.apiKeyName')}
|
||||
onChange={(e) => setAPIKeyName(e.target.value)}
|
||||
borderVariant="thin"
|
||||
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||
></Input>
|
||||
</div>
|
||||
<div className="my-4">
|
||||
<Dropdown
|
||||
placeholder={t('modals.createAPIKey.sourceDoc')}
|
||||
selectedValue={sourcePath ? sourcePath.name : null}
|
||||
onSelect={(selection: { name: string; id: string; type: string }) => {
|
||||
setSourcePath(selection);
|
||||
}}
|
||||
options={extractDocPaths()}
|
||||
size="w-full"
|
||||
rounded="xl"
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
<div className="my-4">
|
||||
<Dropdown
|
||||
options={activePrompts}
|
||||
selectedValue={prompt ? prompt.name : null}
|
||||
placeholder={t('modals.createAPIKey.prompt')}
|
||||
onSelect={(value: { name: string; id: string; type: string }) =>
|
||||
setPrompt(value)
|
||||
}
|
||||
size="w-full"
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
<div className="my-4">
|
||||
<p className="text-jet dark:text-bright-gray mb-2 ml-2 font-semibold">
|
||||
{t('modals.createAPIKey.chunks')}
|
||||
</p>
|
||||
<Dropdown
|
||||
options={chunkOptions}
|
||||
selectedValue={chunk}
|
||||
onSelect={(value: string) => setChunk(value)}
|
||||
size="w-full"
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
disabled={!sourcePath || APIKeyName.length === 0 || !prompt}
|
||||
onClick={() => {
|
||||
if (sourcePath && prompt) {
|
||||
const payload: any = {
|
||||
name: APIKeyName,
|
||||
prompt_id: prompt.id,
|
||||
chunks: chunk,
|
||||
};
|
||||
if (sourcePath.type === 'default') {
|
||||
payload.retriever = sourcePath.id;
|
||||
}
|
||||
if (sourcePath.type === 'local') {
|
||||
payload.source = sourcePath.id;
|
||||
}
|
||||
createAPIKey(payload);
|
||||
}
|
||||
}}
|
||||
className="bg-purple-30 hover:bg-violets-are-blue float-right mt-4 rounded-full px-5 py-2 text-sm text-white disabled:opacity-50"
|
||||
>
|
||||
{t('modals.createAPIKey.create')}
|
||||
</button>
|
||||
</WrapperModal>
|
||||
);
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export default function DeleteConvModal({
|
||||
<ConfirmationModal
|
||||
message={t('modals.deleteConv.confirm')}
|
||||
modalState={modalState}
|
||||
setModalState={setModalState}
|
||||
setModalState={(state) => dispatch(setModalState(state))}
|
||||
submitLabel={t('modals.deleteConv.delete')}
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={handleCancel}
|
||||
|
||||
@@ -18,14 +18,6 @@ interface MCPServerModalProps {
|
||||
onServerSaved: () => void;
|
||||
}
|
||||
|
||||
const authTypes = [
|
||||
{ label: 'No Authentication', value: 'none' },
|
||||
{ label: 'API Key', value: 'api_key' },
|
||||
{ label: 'Bearer Token', value: 'bearer' },
|
||||
{ label: 'OAuth', value: 'oauth' },
|
||||
// { label: 'Basic Authentication', value: 'basic' },
|
||||
];
|
||||
|
||||
export default function MCPServerModal({
|
||||
modalState,
|
||||
setModalState,
|
||||
@@ -36,8 +28,16 @@ export default function MCPServerModal({
|
||||
const token = useSelector(selectToken);
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const authTypes = [
|
||||
{ label: t('settings.tools.mcp.authTypes.none'), value: 'none' },
|
||||
{ label: t('settings.tools.mcp.authTypes.apiKey'), value: 'api_key' },
|
||||
{ label: t('settings.tools.mcp.authTypes.bearer'), value: 'bearer' },
|
||||
{ label: t('settings.tools.mcp.authTypes.oauth'), value: 'oauth' },
|
||||
// { label: t('settings.tools.mcp.authTypes.basic'), value: 'basic' },
|
||||
];
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: server?.displayName || 'My MCP Server',
|
||||
name: server?.displayName || t('settings.tools.mcp.defaultServerName'),
|
||||
server_url: server?.server_url || '',
|
||||
auth_type: server?.auth_type || 'none',
|
||||
api_key: '',
|
||||
@@ -72,7 +72,7 @@ export default function MCPServerModal({
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
name: 'My MCP Server',
|
||||
name: t('settings.tools.mcp.defaultServerName'),
|
||||
server_url: '',
|
||||
auth_type: 'none',
|
||||
api_key: '',
|
||||
@@ -133,7 +133,7 @@ export default function MCPServerModal({
|
||||
typeof timeoutValue === 'number' &&
|
||||
(timeoutValue < 1 || timeoutValue > 300)
|
||||
)
|
||||
newErrors.timeout = 'Timeout must be between 1 and 300 seconds';
|
||||
newErrors.timeout = t('settings.tools.mcp.errors.timeoutRange');
|
||||
|
||||
if (authFieldChecks[formData.auth_type])
|
||||
authFieldChecks[formData.auth_type]();
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { SaveAPIKeyModalProps } from '../models/misc';
|
||||
import WrapperModal from './WrapperModal';
|
||||
|
||||
export default function SaveAPIKeyModal({
|
||||
apiKey,
|
||||
close,
|
||||
}: SaveAPIKeyModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isCopied, setIsCopied] = React.useState(false);
|
||||
|
||||
const handleCopyKey = () => {
|
||||
navigator.clipboard.writeText(apiKey);
|
||||
setIsCopied(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<WrapperModal close={close}>
|
||||
<h1 className="text-jet dark:text-bright-gray my-0 text-xl font-medium">
|
||||
{t('modals.saveKey.note')}
|
||||
</h1>
|
||||
<h3 className="text-outer-space dark:text-silver text-sm font-normal">
|
||||
{t('modals.saveKey.disclaimer')}
|
||||
</h3>
|
||||
<div className="flex justify-between py-2">
|
||||
<div>
|
||||
<h2 className="text-jet dark:text-bright-gray text-base font-semibold">
|
||||
API Key
|
||||
</h2>
|
||||
<span className="text-jet dark:text-bright-gray text-sm leading-7 font-normal">
|
||||
{apiKey}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="border-violets-are-blue text-violets-are-blue hover:bg-violets-are-blue my-1 h-10 w-20 rounded-full border border-solid p-2 text-sm hover:text-white"
|
||||
onClick={handleCopyKey}
|
||||
>
|
||||
{isCopied ? t('modals.saveKey.copied') : t('modals.saveKey.copy')}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={close}
|
||||
className="bg-philippine-yellow rounded-full px-4 py-3 font-medium text-black hover:bg-[#E6B91A]"
|
||||
>
|
||||
{t('modals.saveKey.confirm')}
|
||||
</button>
|
||||
</WrapperModal>
|
||||
);
|
||||
}
|
||||
@@ -43,6 +43,7 @@ export default function WrapperModal({
|
||||
|
||||
const modalContent = (
|
||||
<div className="fixed top-0 left-0 z-30 flex h-screen w-screen items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/25 backdrop-blur-xs dark:bg-black/50" />
|
||||
<div
|
||||
ref={modalRef}
|
||||
className={`relative rounded-2xl bg-white p-8 shadow-[0px_4px_40px_-3px_#0000001A] dark:bg-[#26272E] ${className}`}
|
||||
|
||||
@@ -39,18 +39,3 @@ export type DocumentsProps = {
|
||||
paginatedDocuments: Doc[] | null;
|
||||
handleDeleteDocument: (index: number, document: Doc) => void;
|
||||
};
|
||||
|
||||
export type CreateAPIKeyModalProps = {
|
||||
close: () => void;
|
||||
createAPIKey: (payload: {
|
||||
name: string;
|
||||
source: string;
|
||||
prompt_id: string;
|
||||
chunks: string;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export type SaveAPIKeyModalProps = {
|
||||
apiKey: string;
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,216 @@
|
||||
import { ActiveState } from '../models/misc';
|
||||
import Input from '../components/Input';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import WrapperModal from '../modals/WrapperModal';
|
||||
import Dropdown from '../components/Dropdown';
|
||||
import BookIcon from '../assets/book.svg';
|
||||
import userService from '../api/services/userService';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import { UserToolType } from '../settings/types';
|
||||
|
||||
const variablePattern = /(\{\{\s*[^{}]+\s*\}\}|\{(?!\{)[^{}]+\})/g;
|
||||
|
||||
const escapeHtml = (value: string) =>
|
||||
value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
const highlightPromptVariables = (text: string) => {
|
||||
if (!text) {
|
||||
return '​';
|
||||
}
|
||||
variablePattern.lastIndex = 0;
|
||||
let result = '';
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = variablePattern.exec(text)) !== null) {
|
||||
const precedingText = text.slice(lastIndex, match.index);
|
||||
if (precedingText) {
|
||||
result += escapeHtml(precedingText);
|
||||
}
|
||||
result += `<span class="prompt-variable-highlight">${escapeHtml(match[0])}</span>`;
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
const remainingText = text.slice(lastIndex);
|
||||
if (remainingText) {
|
||||
result += escapeHtml(remainingText);
|
||||
}
|
||||
|
||||
return result || '​';
|
||||
};
|
||||
|
||||
const systemVariableOptionDefinitions = [
|
||||
{
|
||||
labelKey: 'modals.prompts.systemVariableOptions.sourceContent',
|
||||
value: 'source.content',
|
||||
},
|
||||
{
|
||||
labelKey: 'modals.prompts.systemVariableOptions.sourceSummaries',
|
||||
value: 'source.summaries',
|
||||
},
|
||||
{
|
||||
labelKey: 'modals.prompts.systemVariableOptions.sourceDocuments',
|
||||
value: 'source.documents',
|
||||
},
|
||||
{
|
||||
labelKey: 'modals.prompts.systemVariableOptions.sourceCount',
|
||||
value: 'source.count',
|
||||
},
|
||||
{
|
||||
labelKey: 'modals.prompts.systemVariableOptions.systemDate',
|
||||
value: 'system.date',
|
||||
},
|
||||
{
|
||||
labelKey: 'modals.prompts.systemVariableOptions.systemTime',
|
||||
value: 'system.time',
|
||||
},
|
||||
{
|
||||
labelKey: 'modals.prompts.systemVariableOptions.systemTimestamp',
|
||||
value: 'system.timestamp',
|
||||
},
|
||||
{
|
||||
labelKey: 'modals.prompts.systemVariableOptions.systemRequestId',
|
||||
value: 'system.request_id',
|
||||
},
|
||||
{
|
||||
labelKey: 'modals.prompts.systemVariableOptions.systemUserId',
|
||||
value: 'system.user_id',
|
||||
},
|
||||
];
|
||||
|
||||
const buildSystemVariableOptions = (translate: (key: string) => string) =>
|
||||
systemVariableOptionDefinitions.map(({ value, labelKey }) => ({
|
||||
value,
|
||||
label: translate(labelKey),
|
||||
}));
|
||||
|
||||
type PromptTextareaProps = {
|
||||
id: string;
|
||||
value: string;
|
||||
onChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
ariaLabel: string;
|
||||
};
|
||||
|
||||
function PromptTextarea({
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
ariaLabel,
|
||||
}: PromptTextareaProps) {
|
||||
const [scrollOffsets, setScrollOffsets] = React.useState({ top: 0, left: 0 });
|
||||
const highlightedValue = React.useMemo(
|
||||
() => highlightPromptVariables(value),
|
||||
[value],
|
||||
);
|
||||
|
||||
const handleScroll = (event: React.UIEvent<HTMLTextAreaElement>) => {
|
||||
const { scrollTop, scrollLeft } = event.currentTarget;
|
||||
setScrollOffsets({
|
||||
top: scrollTop,
|
||||
left: scrollLeft,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 z-0 overflow-hidden rounded bg-white px-3 py-2 dark:bg-[#26272E]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
className="min-h-full text-base leading-[1.5] break-words whitespace-pre-wrap text-transparent"
|
||||
style={{
|
||||
transform: `translate(${-scrollOffsets.left}px, ${-scrollOffsets.top}px)`,
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: highlightedValue }}
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
id={id}
|
||||
className="peer border-silver dark:border-silver/40 relative z-10 h-48 w-full resize-none rounded border-2 bg-transparent px-3 py-2 text-base text-gray-800 outline-none dark:bg-transparent dark:text-white"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onScroll={handleScroll}
|
||||
placeholder=" "
|
||||
aria-label={ariaLabel}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Custom hook for fetching tool variables
|
||||
const useToolVariables = () => {
|
||||
const token = useSelector(selectToken);
|
||||
const [toolVariables, setToolVariables] = React.useState<
|
||||
{ label: string; value: string }[]
|
||||
>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchToolVariables = async () => {
|
||||
try {
|
||||
const response = await userService.getUserTools(token);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.tools) {
|
||||
const filteredActions: { label: string; value: string }[] = [];
|
||||
|
||||
data.tools.forEach((tool: UserToolType) => {
|
||||
if (tool.actions && tool.status) {
|
||||
// Only include active tools
|
||||
tool.actions.forEach((action: any) => {
|
||||
if (action.active) {
|
||||
const canUseAction =
|
||||
!action.parameters?.properties ||
|
||||
Object.entries(action.parameters.properties).every(
|
||||
([paramName, param]: [string, any]) => {
|
||||
// Parameter is usable if:
|
||||
// 1. It's filled by LLM (true) OR
|
||||
// 2. It has a value in the tool config
|
||||
return (
|
||||
param.filled_by_llm === true ||
|
||||
(tool.config &&
|
||||
tool.config[paramName] &&
|
||||
tool.config[paramName] !== '')
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (canUseAction) {
|
||||
const toolIdentifier = tool.id ?? tool.name;
|
||||
if (!toolIdentifier) {
|
||||
return;
|
||||
}
|
||||
filteredActions.push({
|
||||
label: `${action.name} (${tool.displayName || tool.name})`,
|
||||
value: `tools.${toolIdentifier}.${action.name}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setToolVariables(filteredActions);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching tool variables:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchToolVariables();
|
||||
}, [token]);
|
||||
|
||||
return toolVariables;
|
||||
};
|
||||
|
||||
function AddPrompt({
|
||||
setModalState,
|
||||
@@ -22,52 +230,190 @@ function AddPrompt({
|
||||
disableSave: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const systemVariableOptions = React.useMemo(
|
||||
() => buildSystemVariableOptions(t),
|
||||
[t],
|
||||
);
|
||||
const toolVariables = useToolVariables();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="text-jet dark:text-bright-gray mb-1 text-xl">
|
||||
<p className="mb-1 text-xl font-semibold text-[#2B2B2B] dark:text-white">
|
||||
{t('modals.prompts.addPrompt')}
|
||||
</p>
|
||||
<p className="text-sonic-silver mb-7 text-xs dark:text-[#7F7F82]">
|
||||
<p className="mb-6 text-sm text-[#6B6B6B] dark:text-[#9A9AA0]">
|
||||
{t('modals.prompts.addDescription')}
|
||||
</p>
|
||||
<div>
|
||||
<Input
|
||||
placeholder={t('modals.prompts.promptName')}
|
||||
type="text"
|
||||
className="mb-4"
|
||||
className="mb-5"
|
||||
edgeRoundness="rounded"
|
||||
textSize="medium"
|
||||
value={newPromptName}
|
||||
onChange={(e) => setNewPromptName(e.target.value)}
|
||||
labelBgClassName="bg-white dark:bg-[#26272E]"
|
||||
borderVariant="thin"
|
||||
borderVariant="thick"
|
||||
/>
|
||||
<div className="relative top-[7px] left-3">
|
||||
<span className="text-silver dark:text-silver bg-white px-1 text-xs dark:bg-[#26272E]">
|
||||
|
||||
<div className="relative w-full">
|
||||
<PromptTextarea
|
||||
id="new-prompt-content"
|
||||
value={newPromptContent}
|
||||
onChange={(e) => setNewPromptContent(e.target.value)}
|
||||
ariaLabel={t('prompts.textAriaLabel')}
|
||||
/>
|
||||
<label
|
||||
htmlFor="new-prompt-content"
|
||||
className={`absolute z-20 select-none ${
|
||||
newPromptContent ? '-top-2.5 left-3 text-xs' : ''
|
||||
} text-gray-4000 pointer-events-none max-w-[calc(100%-24px)] cursor-none overflow-hidden bg-white px-2 text-ellipsis whitespace-nowrap transition-all peer-placeholder-shown:top-2.5 peer-placeholder-shown:left-3 peer-placeholder-shown:text-base peer-focus:-top-2.5 peer-focus:left-3 peer-focus:text-xs dark:bg-[#26272E] dark:text-gray-400`}
|
||||
>
|
||||
{t('modals.prompts.promptText')}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<label htmlFor="new-prompt-content" className="sr-only">
|
||||
{t('modals.prompts.promptText')}
|
||||
</label>
|
||||
<textarea
|
||||
id="new-prompt-content"
|
||||
className="border-silver dark:border-silver/40 h-56 w-full resize-none rounded-lg border-2 px-3 py-2 outline-hidden dark:bg-transparent dark:text-white"
|
||||
value={newPromptContent}
|
||||
onChange={(e) => setNewPromptContent(e.target.value)}
|
||||
aria-label="Prompt Text"
|
||||
></textarea>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-row-reverse">
|
||||
<button
|
||||
onClick={handleAddPrompt}
|
||||
className="bg-purple-30 hover:bg-violets-are-blue disabled:hover:bg-purple-30 rounded-3xl px-5 py-2 text-sm text-white transition-all"
|
||||
disabled={disableSave}
|
||||
title={
|
||||
disableSave && newPromptName ? t('modals.prompts.nameExists') : ''
|
||||
}
|
||||
>
|
||||
{t('modals.prompts.save')}
|
||||
</button>
|
||||
|
||||
<div className="mt-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center sm:gap-4">
|
||||
<p className="flex flex-col text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<span className="font-bold">
|
||||
{t('modals.prompts.variablesLabel')}
|
||||
</span>
|
||||
<span className="text-xs text-[10px] font-medium text-gray-500">
|
||||
{t('modals.prompts.variablesDescription')}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
|
||||
<Dropdown
|
||||
options={systemVariableOptions}
|
||||
selectedValue={t('modals.prompts.systemVariablesDropdownLabel')}
|
||||
onSelect={(option) => {
|
||||
const textarea = document.getElementById(
|
||||
'new-prompt-content',
|
||||
) as HTMLTextAreaElement;
|
||||
if (textarea) {
|
||||
const cursorPosition = textarea.selectionStart;
|
||||
const textBefore = newPromptContent.slice(0, cursorPosition);
|
||||
const textAfter = newPromptContent.slice(cursorPosition);
|
||||
|
||||
// Add leading space if needed
|
||||
const needsSpace =
|
||||
cursorPosition > 0 &&
|
||||
newPromptContent.charAt(cursorPosition - 1) !== ' ';
|
||||
|
||||
const newText =
|
||||
textBefore +
|
||||
(needsSpace ? ' ' : '') +
|
||||
`{{ ${option.value} }}` +
|
||||
textAfter;
|
||||
setNewPromptContent(newText);
|
||||
|
||||
setTimeout(() => {
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(
|
||||
cursorPosition +
|
||||
option.value.length +
|
||||
6 +
|
||||
(needsSpace ? 1 : 0),
|
||||
cursorPosition +
|
||||
option.value.length +
|
||||
6 +
|
||||
(needsSpace ? 1 : 0),
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
}}
|
||||
placeholder={t('modals.prompts.systemVariablesDropdownLabel')}
|
||||
size="w-[140px] sm:w-[185px]"
|
||||
rounded="3xl"
|
||||
border="border"
|
||||
contentSize="text-[12px] sm:text-[14px]"
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
options={toolVariables}
|
||||
selectedValue={'Tool Variables'}
|
||||
onSelect={(option) => {
|
||||
const textarea = document.getElementById(
|
||||
'new-prompt-content',
|
||||
) as HTMLTextAreaElement;
|
||||
if (textarea) {
|
||||
const cursorPosition = textarea.selectionStart;
|
||||
const textBefore = newPromptContent.slice(0, cursorPosition);
|
||||
const textAfter = newPromptContent.slice(cursorPosition);
|
||||
|
||||
// Add leading space if needed
|
||||
const needsSpace =
|
||||
cursorPosition > 0 &&
|
||||
newPromptContent.charAt(cursorPosition - 1) !== ' ';
|
||||
|
||||
const newText =
|
||||
textBefore +
|
||||
(needsSpace ? ' ' : '') +
|
||||
`{{ ${option.value} }}` +
|
||||
textAfter;
|
||||
setNewPromptContent(newText);
|
||||
setTimeout(() => {
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(
|
||||
cursorPosition +
|
||||
option.value.length +
|
||||
6 +
|
||||
(needsSpace ? 1 : 0),
|
||||
cursorPosition +
|
||||
option.value.length +
|
||||
6 +
|
||||
(needsSpace ? 1 : 0),
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
}}
|
||||
placeholder="Tool Variables"
|
||||
size="w-[140px] sm:w-[171px]"
|
||||
rounded="3xl"
|
||||
border="border"
|
||||
contentSize="text-[12px] sm:text-[14px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col justify-between gap-4 text-[14px] sm:flex-row sm:gap-0">
|
||||
<div className="flex justify-start">
|
||||
<Link
|
||||
to="https://docs.docsgpt.cloud/Guides/Customising-prompts"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-sm font-medium text-[#6A4DF4] hover:underline"
|
||||
>
|
||||
<img
|
||||
src={BookIcon}
|
||||
alt=""
|
||||
className="flex h-4 w-3 flex-shrink-0 items-center justify-center"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-[14px] font-bold">
|
||||
{t('modals.prompts.learnAboutPrompts')}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 sm:gap-4">
|
||||
<button
|
||||
onClick={() => setModalState('INACTIVE')}
|
||||
className="rounded-3xl border border-[#D9534F] px-5 py-2 text-sm font-medium text-[#D9534F] transition-all hover:bg-[#D9534F] hover:text-white"
|
||||
>
|
||||
{t('modals.prompts.cancel')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleAddPrompt}
|
||||
className="rounded-3xl bg-[#6A4DF4] px-6 py-2 text-sm font-medium text-white transition-all hover:bg-[#563DD1] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-[#6A4DF4]"
|
||||
disabled={disableSave}
|
||||
>
|
||||
{t('modals.prompts.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -93,54 +439,193 @@ function EditPrompt({
|
||||
disableSave: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const systemVariableOptions = React.useMemo(
|
||||
() => buildSystemVariableOptions(t),
|
||||
[t],
|
||||
);
|
||||
const toolVariables = useToolVariables();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="">
|
||||
<p className="text-jet dark:text-bright-gray mb-1 text-xl">
|
||||
{t('modals.prompts.editPrompt')}
|
||||
</p>
|
||||
<p className="text-sonic-silver mb-7 text-xs dark:text-[#7F7F82]">
|
||||
{t('modals.prompts.editDescription')}
|
||||
</p>
|
||||
<div>
|
||||
<Input
|
||||
placeholder={t('modals.prompts.promptName')}
|
||||
type="text"
|
||||
className="mb-4"
|
||||
value={editPromptName}
|
||||
onChange={(e) => setEditPromptName(e.target.value)}
|
||||
labelBgClassName="bg-white dark:bg-charleston-green-2"
|
||||
borderVariant="thin"
|
||||
/>
|
||||
<div className="relative top-[7px] left-3">
|
||||
<span className="text-silver dark:bg-charleston-green-2 dark:text-silver bg-white px-1 text-xs">
|
||||
{t('modals.prompts.promptText')}
|
||||
</span>
|
||||
</div>
|
||||
<label htmlFor="edit-prompt-content" className="sr-only">
|
||||
{t('modals.prompts.promptText')}
|
||||
</label>
|
||||
<textarea
|
||||
<p className="mb-1 text-xl font-semibold text-[#2B2B2B] dark:text-white">
|
||||
{t('modals.prompts.editPrompt')}
|
||||
</p>
|
||||
<p className="mb-6 text-sm text-[#6B6B6B] dark:text-[#9A9AA0]">
|
||||
{t('modals.prompts.editDescription')}
|
||||
</p>
|
||||
<div>
|
||||
<Input
|
||||
placeholder={t('modals.prompts.promptName')}
|
||||
type="text"
|
||||
className="mb-5"
|
||||
edgeRoundness="rounded"
|
||||
textSize="medium"
|
||||
value={editPromptName}
|
||||
onChange={(e) => setEditPromptName(e.target.value)}
|
||||
labelBgClassName="bg-white dark:bg-[#26272E]"
|
||||
borderVariant="thick"
|
||||
/>
|
||||
|
||||
<div className="relative w-full">
|
||||
<PromptTextarea
|
||||
id="edit-prompt-content"
|
||||
className="border-silver dark:border-silver/40 h-56 w-full resize-none rounded-lg border-2 px-3 py-2 outline-hidden dark:bg-transparent dark:text-white"
|
||||
value={editPromptContent}
|
||||
onChange={(e) => setEditPromptContent(e.target.value)}
|
||||
aria-label="Prompt Text"
|
||||
></textarea>
|
||||
ariaLabel={t('prompts.textAriaLabel')}
|
||||
/>
|
||||
<label
|
||||
htmlFor="edit-prompt-content"
|
||||
className={`absolute z-20 select-none ${
|
||||
editPromptContent ? '-top-2.5 left-3 text-xs' : ''
|
||||
} text-gray-4000 pointer-events-none max-w-[calc(100%-24px)] cursor-none overflow-hidden bg-white px-2 text-ellipsis whitespace-nowrap transition-all peer-placeholder-shown:top-2.5 peer-placeholder-shown:left-3 peer-placeholder-shown:text-base peer-focus:-top-2.5 peer-focus:left-3 peer-focus:text-xs dark:bg-[#26272E] dark:text-gray-400`}
|
||||
>
|
||||
{t('modals.prompts.promptText')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-row-reverse gap-4">
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center sm:gap-4">
|
||||
<p className="flex flex-col text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<span className="font-bold">
|
||||
{t('modals.prompts.variablesLabel')}
|
||||
</span>
|
||||
<span className="text-xs text-[10px] font-medium text-gray-500">
|
||||
{t('modals.prompts.variablesDescription')}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
|
||||
<Dropdown
|
||||
options={systemVariableOptions}
|
||||
selectedValue={t('modals.prompts.systemVariablesDropdownLabel')}
|
||||
onSelect={(option) => {
|
||||
const textarea = document.getElementById(
|
||||
'edit-prompt-content',
|
||||
) as HTMLTextAreaElement;
|
||||
if (textarea) {
|
||||
const cursorPosition = textarea.selectionStart;
|
||||
const textBefore = editPromptContent.slice(0, cursorPosition);
|
||||
const textAfter = editPromptContent.slice(cursorPosition);
|
||||
|
||||
// Add leading space if needed
|
||||
const needsSpace =
|
||||
cursorPosition > 0 &&
|
||||
editPromptContent.charAt(cursorPosition - 1) !== ' ';
|
||||
|
||||
const newText =
|
||||
textBefore +
|
||||
(needsSpace ? ' ' : '') +
|
||||
`{{ ${option.value} }}` +
|
||||
textAfter;
|
||||
setEditPromptContent(newText);
|
||||
|
||||
setTimeout(() => {
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(
|
||||
cursorPosition +
|
||||
option.value.length +
|
||||
6 +
|
||||
(needsSpace ? 1 : 0),
|
||||
cursorPosition +
|
||||
option.value.length +
|
||||
6 +
|
||||
(needsSpace ? 1 : 0),
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
}}
|
||||
placeholder={t('modals.prompts.systemVariablesDropdownLabel')}
|
||||
size="w-[140px] sm:w-[185px]"
|
||||
rounded="3xl"
|
||||
border="border"
|
||||
contentSize="text-[12px] sm:text-[14px]"
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
options={toolVariables}
|
||||
selectedValue={'Tool Variables'}
|
||||
onSelect={(option) => {
|
||||
const textarea = document.getElementById(
|
||||
'edit-prompt-content',
|
||||
) as HTMLTextAreaElement;
|
||||
if (textarea) {
|
||||
const cursorPosition = textarea.selectionStart;
|
||||
const textBefore = editPromptContent.slice(0, cursorPosition);
|
||||
const textAfter = editPromptContent.slice(cursorPosition);
|
||||
|
||||
// Add leading space if needed
|
||||
const needsSpace =
|
||||
cursorPosition > 0 &&
|
||||
editPromptContent.charAt(cursorPosition - 1) !== ' ';
|
||||
|
||||
const newText =
|
||||
textBefore +
|
||||
(needsSpace ? ' ' : '') +
|
||||
`{{ ${option.value} }}` +
|
||||
textAfter;
|
||||
setEditPromptContent(newText);
|
||||
setTimeout(() => {
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(
|
||||
cursorPosition +
|
||||
option.value.length +
|
||||
6 +
|
||||
(needsSpace ? 1 : 0),
|
||||
cursorPosition +
|
||||
option.value.length +
|
||||
6 +
|
||||
(needsSpace ? 1 : 0),
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
}}
|
||||
placeholder="Tool Variables"
|
||||
size="w-[140px] sm:w-[171px]"
|
||||
rounded="3xl"
|
||||
border="border"
|
||||
contentSize="text-[12px] sm:text-[14px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col justify-between gap-4 text-[14px] sm:flex-row sm:gap-0">
|
||||
<div className="flex justify-start">
|
||||
<Link
|
||||
to="https://docs.docsgpt.cloud/Guides/Customising-prompts"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-sm font-medium text-[#6A4DF4] hover:underline"
|
||||
>
|
||||
<img
|
||||
src={BookIcon}
|
||||
alt=""
|
||||
className="flex h-4 w-3 flex-shrink-0 items-center justify-center"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-[14px] font-bold">
|
||||
{t('modals.prompts.learnAboutPrompts')}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 sm:gap-4">
|
||||
<button
|
||||
onClick={() => setModalState('INACTIVE')}
|
||||
className="rounded-3xl border border-[#D9534F] px-5 py-2 text-sm font-medium text-[#D9534F] transition-all hover:bg-[#D9534F] hover:text-white"
|
||||
>
|
||||
{t('modals.prompts.cancel')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`bg-purple-30 hover:bg-violets-are-blue disabled:hover:bg-purple-30 rounded-3xl px-5 py-2 text-sm text-white transition-all ${
|
||||
currentPromptEdit.type === 'public'
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
handleEditPrompt &&
|
||||
handleEditPrompt(currentPromptEdit.id, currentPromptEdit.type);
|
||||
}}
|
||||
disabled={currentPromptEdit.type === 'public' || disableSave}
|
||||
className="rounded-3xl bg-[#6A4DF4] px-6 py-2 text-sm font-medium text-white transition-all hover:bg-[#563DD1] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-[#6A4DF4]"
|
||||
disabled={
|
||||
currentPromptEdit.type === 'public' ||
|
||||
disableSave ||
|
||||
!editPromptName
|
||||
}
|
||||
title={
|
||||
disableSave && editPromptName
|
||||
? t('modals.prompts.nameExists')
|
||||
@@ -200,23 +685,28 @@ export default function PromptsModal({
|
||||
(prompt) =>
|
||||
newName === prompt.name && prompt.id !== currentPromptEdit.id,
|
||||
);
|
||||
const nameValid = newName && !nameExists;
|
||||
const contentChanged = editPromptContent !== currentPromptEdit.content;
|
||||
|
||||
setDisableSave(!(nameValid || contentChanged));
|
||||
setDisableSave(
|
||||
!(
|
||||
newName &&
|
||||
!nameExists &&
|
||||
editPromptName &&
|
||||
editPromptContent.trim() !== ''
|
||||
),
|
||||
);
|
||||
setEditPromptName(newName);
|
||||
} else {
|
||||
const nameExists = existingPrompts.find(
|
||||
(prompt) => newName === prompt.name,
|
||||
);
|
||||
setDisableSave(!(newName && !nameExists));
|
||||
setDisableSave(
|
||||
!(newName && !nameExists && newPromptContent.trim() !== ''),
|
||||
);
|
||||
setNewPromptName(newName);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContentChange = (edit: boolean, newContent: string) => {
|
||||
if (edit) {
|
||||
const contentChanged = newContent !== currentPromptEdit.content;
|
||||
const nameValid =
|
||||
editPromptName &&
|
||||
!existingPrompts.find(
|
||||
@@ -224,10 +714,13 @@ export default function PromptsModal({
|
||||
editPromptName === prompt.name &&
|
||||
prompt.id !== currentPromptEdit.id,
|
||||
);
|
||||
|
||||
setDisableSave(!(nameValid || contentChanged));
|
||||
setDisableSave(!(nameValid && newContent.trim() !== ''));
|
||||
setEditPromptContent(newContent);
|
||||
} else {
|
||||
const nameValid =
|
||||
newPromptName &&
|
||||
!existingPrompts.find((prompt) => newPromptName === prompt.name);
|
||||
setDisableSave(!(nameValid && newContent.trim() !== ''));
|
||||
setNewPromptContent(newContent);
|
||||
}
|
||||
};
|
||||
@@ -272,7 +765,8 @@ export default function PromptsModal({
|
||||
setNewPromptContent('');
|
||||
}
|
||||
}}
|
||||
className="mt-24 sm:w-[512px]"
|
||||
className="mx-4 mt-16 w-[95vw] max-w-[650px] rounded-2xl bg-white px-4 py-4 sm:px-6 sm:py-6 md:px-8 md:py-6 dark:bg-[#1E1E2A]"
|
||||
contentClassName="!overflow-visible"
|
||||
>
|
||||
{view}
|
||||
</WrapperModal>
|
||||
|
||||
@@ -90,9 +90,27 @@ export function getLocalApiKey(): string | null {
|
||||
return key;
|
||||
}
|
||||
|
||||
export function getLocalRecentDocs(): Doc[] | null {
|
||||
const docs = localStorage.getItem('DocsGPTRecentDocs');
|
||||
return docs ? (JSON.parse(docs) as Doc[]) : null;
|
||||
export function getLocalRecentDocs(sourceDocs?: Doc[] | null): Doc[] | null {
|
||||
const docsString = localStorage.getItem('DocsGPTRecentDocs');
|
||||
const selectedDocs = docsString ? (JSON.parse(docsString) as Doc[]) : null;
|
||||
|
||||
if (!sourceDocs || !selectedDocs || selectedDocs.length === 0) {
|
||||
return selectedDocs;
|
||||
}
|
||||
const isDocAvailable = (selected: Doc) => {
|
||||
return sourceDocs.some((source) => {
|
||||
if (source.id && selected.id) {
|
||||
return source.id === selected.id;
|
||||
}
|
||||
return source.name === selected.name && source.date === selected.date;
|
||||
});
|
||||
};
|
||||
|
||||
const validDocs = selectedDocs.filter(isDocAvailable);
|
||||
|
||||
setLocalRecentDocs(validDocs.length > 0 ? validDocs : null);
|
||||
|
||||
return validDocs.length > 0 ? validDocs : null;
|
||||
}
|
||||
|
||||
export function getLocalPrompt(): string | null {
|
||||
|
||||
@@ -8,7 +8,11 @@ import {
|
||||
import { Agent } from '../agents/types';
|
||||
import { ActiveState, Doc } from '../models/misc';
|
||||
import { RootState } from '../store';
|
||||
import { setLocalApiKey, setLocalRecentDocs } from './preferenceApi';
|
||||
import {
|
||||
setLocalApiKey,
|
||||
setLocalRecentDocs,
|
||||
getLocalRecentDocs,
|
||||
} from './preferenceApi';
|
||||
|
||||
export interface Preference {
|
||||
apiKey: string;
|
||||
@@ -178,6 +182,22 @@ prefListenerMiddleware.startListening({
|
||||
},
|
||||
});
|
||||
|
||||
prefListenerMiddleware.startListening({
|
||||
matcher: isAnyOf(setSourceDocs),
|
||||
effect: (_action, listenerApi) => {
|
||||
const state = listenerApi.getState() as RootState;
|
||||
const sourceDocs = state.preference.sourceDocs;
|
||||
if (sourceDocs && sourceDocs.length > 0) {
|
||||
const validatedDocs = getLocalRecentDocs(sourceDocs);
|
||||
if (validatedDocs !== null) {
|
||||
listenerApi.dispatch(setSelectedDocs(validatedDocs));
|
||||
} else {
|
||||
listenerApi.dispatch(setSelectedDocs([]));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const selectApiKey = (state: RootState) => state.preference.apiKey;
|
||||
export const selectApiKeyStatus = (state: RootState) =>
|
||||
!!state.preference.apiKey;
|
||||
|
||||
@@ -27,8 +27,8 @@ import {
|
||||
} from '../preferences/preferenceSlice';
|
||||
import Upload from '../upload/Upload';
|
||||
import { formatDate } from '../utils/dateTimeUtils';
|
||||
import FileTreeComponent from '../components/FileTreeComponent';
|
||||
import ConnectorTreeComponent from '../components/ConnectorTreeComponent';
|
||||
import FileTree from '../components/FileTree';
|
||||
import ConnectorTree from '../components/ConnectorTree';
|
||||
import Chunks from '../components/Chunks';
|
||||
|
||||
const formatTokens = (tokens: number): string => {
|
||||
@@ -273,13 +273,13 @@ export default function Sources({
|
||||
<div className="mt-8 flex flex-col">
|
||||
{documentToView.isNested ? (
|
||||
documentToView.type === 'connector:file' ? (
|
||||
<ConnectorTreeComponent
|
||||
<ConnectorTree
|
||||
docId={documentToView.id || ''}
|
||||
sourceName={documentToView.name}
|
||||
onBackToDocuments={() => setDocumentToView(undefined)}
|
||||
/>
|
||||
) : (
|
||||
<FileTreeComponent
|
||||
<FileTree
|
||||
docId={documentToView.id || ''}
|
||||
sourceName={documentToView.name}
|
||||
onBackToDocuments={() => setDocumentToView(undefined)}
|
||||
|
||||
@@ -192,7 +192,7 @@ export default function Tools() {
|
||||
<div className="flex w-full flex-col items-center justify-center py-12">
|
||||
<img
|
||||
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
|
||||
alt="No tools found"
|
||||
alt={t('settings.tools.noToolsFound')}
|
||||
className="mx-auto mb-6 h-32 w-32"
|
||||
/>
|
||||
<p className="text-center text-lg text-gray-500 dark:text-gray-400">
|
||||
|
||||
114
scripts/migrate_conversation_id_dbref_to_objectid.py
Normal file
114
scripts/migrate_conversation_id_dbref_to_objectid.py
Normal file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to convert conversation_id from DBRef to ObjectId in shared_conversations collection.
|
||||
"""
|
||||
|
||||
import pymongo
|
||||
import logging
|
||||
from tqdm import tqdm
|
||||
from bson.dbref import DBRef
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger()
|
||||
|
||||
# Configuration
|
||||
MONGO_URI = "mongodb://localhost:27017/"
|
||||
DB_NAME = "docsgpt"
|
||||
|
||||
def backup_collection(collection, backup_collection_name):
|
||||
"""Backup collection before migration."""
|
||||
logger.info(f"Backing up collection {collection.name} to {backup_collection_name}")
|
||||
collection.aggregate([{"$out": backup_collection_name}])
|
||||
logger.info("Backup completed")
|
||||
|
||||
def migrate_conversation_id_dbref_to_objectid():
|
||||
"""Migrate conversation_id from DBRef to ObjectId."""
|
||||
client = pymongo.MongoClient(MONGO_URI)
|
||||
db = client[DB_NAME]
|
||||
shared_conversations_collection = db["shared_conversations"]
|
||||
|
||||
try:
|
||||
# Backup collection before migration
|
||||
backup_collection(shared_conversations_collection, "shared_conversations_backup")
|
||||
|
||||
# Find all documents and filter for DBRef conversation_id in Python
|
||||
all_documents = list(shared_conversations_collection.find({}))
|
||||
documents_with_dbref = []
|
||||
|
||||
for doc in all_documents:
|
||||
conversation_id_field = doc.get("conversation_id")
|
||||
if isinstance(conversation_id_field, DBRef):
|
||||
documents_with_dbref.append(doc)
|
||||
|
||||
if not documents_with_dbref:
|
||||
logger.info("No documents with DBRef conversation_id found. Migration not needed.")
|
||||
return
|
||||
|
||||
logger.info(f"Found {len(documents_with_dbref)} documents with DBRef conversation_id")
|
||||
|
||||
# Process each document
|
||||
migrated_count = 0
|
||||
error_count = 0
|
||||
|
||||
for doc in tqdm(documents_with_dbref, desc="Migrating conversation_id"):
|
||||
try:
|
||||
conversation_id_field = doc.get("conversation_id")
|
||||
|
||||
# Extract the ObjectId from the DBRef
|
||||
dbref_id = conversation_id_field.id
|
||||
|
||||
if dbref_id and ObjectId.is_valid(dbref_id):
|
||||
# Update the document to use direct ObjectId
|
||||
result = shared_conversations_collection.update_one(
|
||||
{"_id": doc["_id"]},
|
||||
{"$set": {"conversation_id": dbref_id}}
|
||||
)
|
||||
|
||||
if result.modified_count > 0:
|
||||
migrated_count += 1
|
||||
logger.debug(f"Successfully migrated document {doc['_id']}")
|
||||
else:
|
||||
error_count += 1
|
||||
logger.warning(f"Failed to update document {doc['_id']}")
|
||||
else:
|
||||
error_count += 1
|
||||
logger.warning(f"Invalid ObjectId in DBRef for document {doc['_id']}: {dbref_id}")
|
||||
|
||||
except Exception as e:
|
||||
error_count += 1
|
||||
logger.error(f"Error migrating document {doc['_id']}: {e}")
|
||||
|
||||
# Final verification
|
||||
all_docs_after = list(shared_conversations_collection.find({}))
|
||||
remaining_dbref = 0
|
||||
for doc in all_docs_after:
|
||||
if isinstance(doc.get("conversation_id"), DBRef):
|
||||
remaining_dbref += 1
|
||||
|
||||
logger.info("Migration completed:")
|
||||
logger.info(f" - Total documents processed: {len(documents_with_dbref)}")
|
||||
logger.info(f" - Successfully migrated: {migrated_count}")
|
||||
logger.info(f" - Errors encountered: {error_count}")
|
||||
logger.info(f" - Remaining DBRef documents: {remaining_dbref}")
|
||||
|
||||
if remaining_dbref == 0:
|
||||
logger.info("✅ Migration successful: All DBRef conversation_id fields have been converted to ObjectId")
|
||||
else:
|
||||
logger.warning(f"⚠️ Migration incomplete: {remaining_dbref} DBRef documents still exist")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Migration failed: {e}")
|
||||
raise
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
logger.info("Starting conversation_id DBRef to ObjectId migration...")
|
||||
migrate_conversation_id_dbref_to_objectid()
|
||||
logger.info("Migration completed successfully!")
|
||||
except Exception as e:
|
||||
logger.error(f"Migration failed due to error: {e}")
|
||||
logger.warning("Please verify database state or restore from backups if necessary.")
|
||||
@@ -84,6 +84,21 @@ def migrate_mongo_atlas_vector_to_v1_vectorstore():
|
||||
logger.info("Mongo Atlas migration completed")
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate_faiss_to_v1_vectorstore()
|
||||
migrate_to_v1_vectorstore_mongo()
|
||||
migrate_mongo_atlas_vector_to_v1_vectorstore()
|
||||
try:
|
||||
logger.info("Starting FAISS migration...")
|
||||
migrate_faiss_to_v1_vectorstore()
|
||||
logger.info("FAISS migration completed successfully ")
|
||||
|
||||
logger.info("Starting local Mongo migration...")
|
||||
migrate_to_v1_vectorstore_mongo()
|
||||
logger.info("Local Mongo migration completed successfully ")
|
||||
|
||||
logger.info("Starting Mongo Atlas migration...")
|
||||
migrate_mongo_atlas_vector_to_v1_vectorstore()
|
||||
logger.info("Mongo Atlas migration completed successfully ")
|
||||
|
||||
logger.info(" All migrations completed successfully!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f" Migration failed due to error: {e}")
|
||||
logger.warning(" Please verify database state or restore from backups if necessary.")
|
||||
|
||||
@@ -64,17 +64,14 @@ class TestBaseAgentBuildMessages:
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
system_prompt = "System: {summaries}"
|
||||
system_prompt = "System prompt content"
|
||||
query = "What is Python?"
|
||||
retrieved_data = [
|
||||
{"text": "Python is a programming language", "filename": "python.txt"}
|
||||
]
|
||||
|
||||
messages = agent._build_messages(system_prompt, query, retrieved_data)
|
||||
messages = agent._build_messages(system_prompt, query)
|
||||
|
||||
assert len(messages) >= 2
|
||||
assert messages[0]["role"] == "system"
|
||||
assert "Python is a programming language" in messages[0]["content"]
|
||||
assert messages[0]["content"] == system_prompt
|
||||
assert messages[-1]["role"] == "user"
|
||||
assert messages[-1]["content"] == query
|
||||
|
||||
@@ -88,11 +85,10 @@ class TestBaseAgentBuildMessages:
|
||||
agent_base_params["chat_history"] = sample_chat_history
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
system_prompt = "System: {summaries}"
|
||||
system_prompt = "System prompt"
|
||||
query = "New question?"
|
||||
retrieved_data = [{"text": "Data", "filename": "file.txt"}]
|
||||
|
||||
messages = agent._build_messages(system_prompt, query, retrieved_data)
|
||||
messages = agent._build_messages(system_prompt, query)
|
||||
|
||||
user_messages = [m for m in messages if m["role"] == "user"]
|
||||
assistant_messages = [m for m in messages if m["role"] == "assistant"]
|
||||
@@ -118,9 +114,7 @@ class TestBaseAgentBuildMessages:
|
||||
agent_base_params["chat_history"] = tool_call_history
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
messages = agent._build_messages(
|
||||
"System: {summaries}", "query", [{"text": "data", "filename": "file.txt"}]
|
||||
)
|
||||
messages = agent._build_messages("System prompt", "query")
|
||||
|
||||
tool_messages = [m for m in messages if m["role"] == "tool"]
|
||||
assert len(tool_messages) > 0
|
||||
@@ -129,32 +123,25 @@ class TestBaseAgentBuildMessages:
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
retrieved_data = [{"text": "Document without filename or title"}]
|
||||
|
||||
messages = agent._build_messages("System: {summaries}", "query", retrieved_data)
|
||||
messages = agent._build_messages("System prompt", "query")
|
||||
|
||||
assert messages[0]["role"] == "system"
|
||||
assert "Document without filename" in messages[0]["content"]
|
||||
assert messages[0]["content"] == "System prompt"
|
||||
|
||||
def test_build_messages_uses_title_as_fallback(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
retrieved_data = [{"text": "Data", "title": "Title Doc"}]
|
||||
|
||||
messages = agent._build_messages("System: {summaries}", "query", retrieved_data)
|
||||
|
||||
assert "Title Doc" in messages[0]["content"]
|
||||
agent._build_messages("System prompt", "query")
|
||||
|
||||
def test_build_messages_uses_source_as_fallback(
|
||||
self, agent_base_params, mock_llm_creator, mock_llm_handler_creator
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
retrieved_data = [{"text": "Data", "source": "source.txt"}]
|
||||
|
||||
messages = agent._build_messages("System: {summaries}", "query", retrieved_data)
|
||||
|
||||
assert "source.txt" in messages[0]["content"]
|
||||
agent._build_messages("System prompt", "query")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -475,40 +462,6 @@ class TestBaseAgentToolExecution:
|
||||
assert truncated[0]["result"].endswith("...")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestBaseAgentRetrieverSearch:
|
||||
|
||||
def test_retriever_search(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
log_context,
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
results = agent._retriever_search(mock_retriever, "test query", log_context)
|
||||
|
||||
assert len(results) == 2
|
||||
mock_retriever.search.assert_called_once_with("test query")
|
||||
|
||||
def test_retriever_search_logs_context(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm_creator,
|
||||
mock_llm_handler_creator,
|
||||
log_context,
|
||||
):
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
agent._retriever_search(mock_retriever, "test query", log_context)
|
||||
|
||||
assert len(log_context.stacks) == 1
|
||||
assert log_context.stacks[0]["component"] == "retriever"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestBaseAgentLLMGeneration:
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ class TestClassicAgent:
|
||||
def test_gen_inner_basic_flow(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
@@ -40,7 +39,7 @@ class TestClassicAgent:
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
results = list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
results = list(agent._gen_inner("Test query", log_context))
|
||||
|
||||
assert len(results) >= 2
|
||||
sources = [r for r in results if "sources" in r]
|
||||
@@ -52,7 +51,6 @@ class TestClassicAgent:
|
||||
def test_gen_inner_retrieves_documents(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
@@ -68,14 +66,11 @@ class TestClassicAgent:
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
|
||||
mock_retriever.search.assert_called_once_with("Test query")
|
||||
list(agent._gen_inner("Test query", log_context))
|
||||
|
||||
def test_gen_inner_uses_user_api_key_tools(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
@@ -104,14 +99,13 @@ class TestClassicAgent:
|
||||
agent_base_params["user_api_key"] = "api_key_123"
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
list(agent._gen_inner("Test query", log_context))
|
||||
|
||||
assert len(agent.tools) >= 0
|
||||
|
||||
def test_gen_inner_uses_user_tools(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
@@ -133,14 +127,13 @@ class TestClassicAgent:
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
list(agent._gen_inner("Test query", log_context))
|
||||
|
||||
assert len(agent.tools) >= 0
|
||||
|
||||
def test_gen_inner_builds_correct_messages(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
@@ -156,7 +149,7 @@ class TestClassicAgent:
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
list(agent._gen_inner("Test query", log_context))
|
||||
|
||||
call_kwargs = mock_llm.gen_stream.call_args[1]
|
||||
messages = call_kwargs["messages"]
|
||||
@@ -169,7 +162,6 @@ class TestClassicAgent:
|
||||
def test_gen_inner_logs_tool_calls(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
@@ -187,7 +179,7 @@ class TestClassicAgent:
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
agent.tool_calls = [{"tool": "test", "result": "success"}]
|
||||
|
||||
list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
list(agent._gen_inner("Test query", log_context))
|
||||
|
||||
agent_logs = [s for s in log_context.stacks if s["component"] == "agent"]
|
||||
assert len(agent_logs) == 1
|
||||
@@ -200,7 +192,6 @@ class TestClassicAgentIntegration:
|
||||
def test_gen_method_with_logging(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
@@ -216,14 +207,13 @@ class TestClassicAgentIntegration:
|
||||
|
||||
agent = ClassicAgent(**agent_base_params)
|
||||
|
||||
results = list(agent.gen("Test query", mock_retriever))
|
||||
results = list(agent.gen("Test query"))
|
||||
|
||||
assert len(results) >= 1
|
||||
|
||||
def test_gen_method_decorator_applied(
|
||||
self,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
|
||||
@@ -35,7 +35,7 @@ class TestReActAgentContentExtraction:
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
|
||||
response = "Simple string response"
|
||||
content = agent._extract_content_from_llm_response(response)
|
||||
content = agent._extract_content(response)
|
||||
|
||||
assert content == "Simple string response"
|
||||
|
||||
@@ -48,7 +48,7 @@ class TestReActAgentContentExtraction:
|
||||
response.message = Mock()
|
||||
response.message.content = "Message content"
|
||||
|
||||
content = agent._extract_content_from_llm_response(response)
|
||||
content = agent._extract_content(response)
|
||||
|
||||
assert content == "Message content"
|
||||
|
||||
@@ -64,7 +64,7 @@ class TestReActAgentContentExtraction:
|
||||
response.message = None
|
||||
response.content = None
|
||||
|
||||
content = agent._extract_content_from_llm_response(response)
|
||||
content = agent._extract_content(response)
|
||||
|
||||
assert content == "OpenAI content"
|
||||
|
||||
@@ -81,7 +81,7 @@ class TestReActAgentContentExtraction:
|
||||
response.message = None
|
||||
response.choices = None
|
||||
|
||||
content = agent._extract_content_from_llm_response(response)
|
||||
content = agent._extract_content(response)
|
||||
|
||||
assert content == "Anthropic content"
|
||||
|
||||
@@ -101,7 +101,7 @@ class TestReActAgentContentExtraction:
|
||||
chunk2.choices[0].delta.content = "Part 2"
|
||||
|
||||
response = iter([chunk1, chunk2])
|
||||
content = agent._extract_content_from_llm_response(response)
|
||||
content = agent._extract_content(response)
|
||||
|
||||
assert content == "Part 1 Part 2"
|
||||
|
||||
@@ -123,7 +123,7 @@ class TestReActAgentContentExtraction:
|
||||
chunk2.choices = []
|
||||
|
||||
response = iter([chunk1, chunk2])
|
||||
content = agent._extract_content_from_llm_response(response)
|
||||
content = agent._extract_content(response)
|
||||
|
||||
assert content == "Stream 1 Stream 2"
|
||||
|
||||
@@ -133,7 +133,7 @@ class TestReActAgentContentExtraction:
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
|
||||
response = iter(["chunk1", "chunk2", "chunk3"])
|
||||
content = agent._extract_content_from_llm_response(response)
|
||||
content = agent._extract_content(response)
|
||||
|
||||
assert content == "chunk1chunk2chunk3"
|
||||
|
||||
@@ -148,7 +148,7 @@ class TestReActAgentContentExtraction:
|
||||
response.choices = None
|
||||
response.content = None
|
||||
|
||||
content = agent._extract_content_from_llm_response(response)
|
||||
content = agent._extract_content(response)
|
||||
|
||||
assert content == ""
|
||||
|
||||
@@ -161,7 +161,7 @@ class TestReActAgentPlanning:
|
||||
new_callable=mock_open,
|
||||
read_data="Test planning prompt: {query} {summaries} {prompt} {observations}",
|
||||
)
|
||||
def test_create_plan(
|
||||
def test_planning_phase(
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
@@ -171,24 +171,27 @@ class TestReActAgentPlanning:
|
||||
log_context,
|
||||
):
|
||||
def mock_gen_stream(*args, **kwargs):
|
||||
yield "Plan step 1"
|
||||
yield "Plan step 2"
|
||||
# Return simple strings - _extract_content handles strings directly
|
||||
|
||||
yield "Plan "
|
||||
yield "content"
|
||||
|
||||
mock_llm.gen_stream = Mock(return_value=mock_gen_stream())
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
agent.observations = ["Observation 1"]
|
||||
|
||||
plan_chunks = list(agent._create_plan("Test query", "Test docs", log_context))
|
||||
plan_chunks = list(agent._planning_phase("Test query", log_context))
|
||||
|
||||
assert len(plan_chunks) == 2
|
||||
assert plan_chunks[0] == "Plan step 1"
|
||||
assert plan_chunks[1] == "Plan step 2"
|
||||
# Should yield thought dicts
|
||||
|
||||
assert any("thought" in chunk for chunk in plan_chunks)
|
||||
assert agent.plan == "Plan content"
|
||||
|
||||
mock_llm.gen_stream.assert_called_once()
|
||||
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="Test: {query}")
|
||||
def test_create_plan_fills_template(
|
||||
def test_planning_phase_fills_template(
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
@@ -197,10 +200,10 @@ class TestReActAgentPlanning:
|
||||
mock_llm_handler_creator,
|
||||
log_context,
|
||||
):
|
||||
mock_llm.gen_stream = Mock(return_value=iter(["Plan"]))
|
||||
mock_llm.gen_stream = Mock(return_value=iter([]))
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
list(agent._create_plan("My query", "Docs", log_context))
|
||||
list(agent._planning_phase("My query", log_context))
|
||||
|
||||
call_args = mock_llm.gen_stream.call_args[1]
|
||||
messages = call_args["messages"]
|
||||
@@ -216,7 +219,7 @@ class TestReActAgentFinalAnswer:
|
||||
new_callable=mock_open,
|
||||
read_data="Final answer for: {query} with {observations}",
|
||||
)
|
||||
def test_create_final_answer(
|
||||
def test_synthesis_phase(
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
@@ -226,24 +229,22 @@ class TestReActAgentFinalAnswer:
|
||||
log_context,
|
||||
):
|
||||
def mock_gen_stream(*args, **kwargs):
|
||||
yield "Final "
|
||||
yield "answer"
|
||||
yield Mock(choices=[Mock(delta=Mock(content="Final "))])
|
||||
yield Mock(choices=[Mock(delta=Mock(content="answer"))])
|
||||
|
||||
mock_llm.gen_stream = Mock(return_value=mock_gen_stream())
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
observations = ["Obs 1", "Obs 2"]
|
||||
agent.observations = ["Obs 1", "Obs 2"]
|
||||
|
||||
answer_chunks = list(
|
||||
agent._create_final_answer("Test query", observations, log_context)
|
||||
)
|
||||
answer_chunks = list(agent._synthesis_phase("Test query", log_context))
|
||||
|
||||
assert len(answer_chunks) == 2
|
||||
assert answer_chunks[0] == "Final "
|
||||
assert answer_chunks[1] == "answer"
|
||||
# Should yield answer dicts
|
||||
|
||||
assert any("answer" in chunk for chunk in answer_chunks)
|
||||
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="Answer: {observations}")
|
||||
def test_create_final_answer_truncates_long_observations(
|
||||
def test_synthesis_phase_truncates_long_observations(
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
@@ -252,20 +253,20 @@ class TestReActAgentFinalAnswer:
|
||||
mock_llm_handler_creator,
|
||||
log_context,
|
||||
):
|
||||
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
||||
mock_llm.gen_stream = Mock(return_value=iter([]))
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
long_obs = ["A" * 15000]
|
||||
agent.observations = ["A" * 15000]
|
||||
|
||||
list(agent._create_final_answer("Query", long_obs, log_context))
|
||||
list(agent._synthesis_phase("Query", log_context))
|
||||
|
||||
call_args = mock_llm.gen_stream.call_args[1]
|
||||
messages = call_args["messages"]
|
||||
|
||||
assert "observations truncated" in messages[0]["content"]
|
||||
assert "truncated" in messages[0]["content"]
|
||||
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="Test: {query}")
|
||||
def test_create_final_answer_no_tools(
|
||||
def test_synthesis_phase_no_tools(
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
@@ -274,10 +275,11 @@ class TestReActAgentFinalAnswer:
|
||||
mock_llm_handler_creator,
|
||||
log_context,
|
||||
):
|
||||
mock_llm.gen_stream = Mock(return_value=iter(["Answer"]))
|
||||
mock_llm.gen_stream = Mock(return_value=iter([]))
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
list(agent._create_final_answer("Query", ["Obs"], log_context))
|
||||
agent.observations = ["Obs"]
|
||||
list(agent._synthesis_phase("Query", log_context))
|
||||
|
||||
call_args = mock_llm.gen_stream.call_args[1]
|
||||
|
||||
@@ -294,7 +296,6 @@ class TestReActAgentGenInner:
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
@@ -313,7 +314,7 @@ class TestReActAgentGenInner:
|
||||
agent.plan = "Old plan"
|
||||
agent.observations = ["Old obs"]
|
||||
|
||||
list(agent._gen_inner("New query", mock_retriever, log_context))
|
||||
list(agent._gen_inner("New query", log_context))
|
||||
|
||||
assert agent.plan != "Old plan"
|
||||
assert len(agent.observations) > 0
|
||||
@@ -323,7 +324,6 @@ class TestReActAgentGenInner:
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
@@ -351,7 +351,7 @@ class TestReActAgentGenInner:
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
results = list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
results = list(agent._gen_inner("Test query", log_context))
|
||||
|
||||
assert any("answer" in r for r in results)
|
||||
|
||||
@@ -360,7 +360,6 @@ class TestReActAgentGenInner:
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
@@ -386,7 +385,7 @@ class TestReActAgentGenInner:
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
|
||||
results = list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
results = list(agent._gen_inner("Test query", log_context))
|
||||
|
||||
thought_results = [r for r in results if "thought" in r]
|
||||
assert len(thought_results) > 0
|
||||
@@ -396,7 +395,6 @@ class TestReActAgentGenInner:
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
@@ -412,7 +410,7 @@ class TestReActAgentGenInner:
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
results = list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
results = list(agent._gen_inner("Test query", log_context))
|
||||
|
||||
sources = [r for r in results if "sources" in r]
|
||||
assert len(sources) >= 1
|
||||
@@ -422,7 +420,6 @@ class TestReActAgentGenInner:
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
@@ -440,7 +437,7 @@ class TestReActAgentGenInner:
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
agent.tool_calls = [{"tool": "test", "result": "A" * 100}]
|
||||
|
||||
results = list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
results = list(agent._gen_inner("Test query", log_context))
|
||||
|
||||
tool_call_results = [r for r in results if "tool_calls" in r]
|
||||
if tool_call_results:
|
||||
@@ -451,7 +448,6 @@ class TestReActAgentGenInner:
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
@@ -467,7 +463,7 @@ class TestReActAgentGenInner:
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
list(agent._gen_inner("Test query", mock_retriever, log_context))
|
||||
list(agent._gen_inner("Test query", log_context))
|
||||
|
||||
assert len(agent.observations) > 0
|
||||
|
||||
@@ -484,7 +480,6 @@ class TestReActAgentIntegration:
|
||||
self,
|
||||
mock_file,
|
||||
agent_base_params,
|
||||
mock_retriever,
|
||||
mock_llm,
|
||||
mock_llm_handler,
|
||||
mock_llm_creator,
|
||||
@@ -512,7 +507,7 @@ class TestReActAgentIntegration:
|
||||
mock_llm_handler.process_message_flow = Mock(side_effect=mock_handler)
|
||||
|
||||
agent = ReActAgent(**agent_base_params)
|
||||
results = list(agent._gen_inner("Complex query", mock_retriever, log_context))
|
||||
results = list(agent._gen_inner("Complex query", log_context))
|
||||
|
||||
assert len(results) > 0
|
||||
assert any("thought" in r for r in results)
|
||||
|
||||
@@ -315,16 +315,12 @@ class TestCompleteStreamMethod:
|
||||
]
|
||||
)
|
||||
|
||||
mock_retriever = MagicMock()
|
||||
mock_retriever.get_params.return_value = {}
|
||||
|
||||
decoded_token = {"sub": "user123"}
|
||||
|
||||
stream = list(
|
||||
resource.complete_stream(
|
||||
question="Test question",
|
||||
agent=mock_agent,
|
||||
retriever=mock_retriever,
|
||||
conversation_id=None,
|
||||
user_api_key=None,
|
||||
decoded_token=decoded_token,
|
||||
@@ -351,16 +347,12 @@ class TestCompleteStreamMethod:
|
||||
]
|
||||
)
|
||||
|
||||
mock_retriever = MagicMock()
|
||||
mock_retriever.get_params.return_value = {}
|
||||
|
||||
decoded_token = {"sub": "user123"}
|
||||
|
||||
stream = list(
|
||||
resource.complete_stream(
|
||||
question="Test?",
|
||||
agent=mock_agent,
|
||||
retriever=mock_retriever,
|
||||
conversation_id=None,
|
||||
user_api_key=None,
|
||||
decoded_token=decoded_token,
|
||||
@@ -381,16 +373,12 @@ class TestCompleteStreamMethod:
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.gen.side_effect = Exception("Test error")
|
||||
|
||||
mock_retriever = MagicMock()
|
||||
mock_retriever.get_params.return_value = {}
|
||||
|
||||
decoded_token = {"sub": "user123"}
|
||||
|
||||
stream = list(
|
||||
resource.complete_stream(
|
||||
question="Test?",
|
||||
agent=mock_agent,
|
||||
retriever=mock_retriever,
|
||||
conversation_id=None,
|
||||
user_api_key=None,
|
||||
decoded_token=decoded_token,
|
||||
@@ -413,9 +401,6 @@ class TestCompleteStreamMethod:
|
||||
]
|
||||
)
|
||||
|
||||
mock_retriever = MagicMock()
|
||||
mock_retriever.get_params.return_value = {}
|
||||
|
||||
decoded_token = {"sub": "user123"}
|
||||
|
||||
with patch.object(
|
||||
@@ -427,8 +412,7 @@ class TestCompleteStreamMethod:
|
||||
resource.complete_stream(
|
||||
question="Test?",
|
||||
agent=mock_agent,
|
||||
retriever=mock_retriever,
|
||||
conversation_id=None,
|
||||
conversation_id=None,
|
||||
user_api_key=None,
|
||||
decoded_token=decoded_token,
|
||||
should_save_conversation=True,
|
||||
@@ -461,7 +445,6 @@ class TestCompleteStreamMethod:
|
||||
resource.complete_stream(
|
||||
question="Test question?",
|
||||
agent=mock_agent,
|
||||
retriever=mock_retriever,
|
||||
conversation_id=None,
|
||||
user_api_key="test_key",
|
||||
decoded_token=decoded_token,
|
||||
|
||||
850
tests/api/answer/services/test_prompt_renderer.py
Normal file
850
tests/api/answer/services/test_prompt_renderer.py
Normal file
@@ -0,0 +1,850 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestTemplateEngine:
|
||||
|
||||
def test_render_simple_template(self):
|
||||
from application.templates.template_engine import TemplateEngine
|
||||
|
||||
engine = TemplateEngine()
|
||||
result = engine.render("Hello {{ name }}", {"name": "World"})
|
||||
|
||||
assert result == "Hello World"
|
||||
|
||||
def test_render_with_namespace(self):
|
||||
from application.templates.template_engine import TemplateEngine
|
||||
|
||||
engine = TemplateEngine()
|
||||
context = {
|
||||
"user": {"name": "Alice", "role": "admin"},
|
||||
"system": {"date": "2025-10-22"},
|
||||
}
|
||||
result = engine.render(
|
||||
"{{ user.name }} is a {{ user.role }} on {{ system.date }}", context
|
||||
)
|
||||
|
||||
assert result == "Alice is a admin on 2025-10-22"
|
||||
|
||||
def test_render_empty_template(self):
|
||||
from application.templates.template_engine import TemplateEngine
|
||||
|
||||
engine = TemplateEngine()
|
||||
result = engine.render("", {"key": "value"})
|
||||
|
||||
assert result == ""
|
||||
|
||||
def test_render_template_without_variables(self):
|
||||
from application.templates.template_engine import TemplateEngine
|
||||
|
||||
engine = TemplateEngine()
|
||||
result = engine.render("Just plain text", {})
|
||||
|
||||
assert result == "Just plain text"
|
||||
|
||||
def test_render_undefined_variable_returns_empty_string(self):
|
||||
from application.templates.template_engine import TemplateEngine
|
||||
|
||||
engine = TemplateEngine()
|
||||
|
||||
result = engine.render("Hello {{ undefined_var }}", {})
|
||||
assert result == "Hello "
|
||||
|
||||
def test_render_syntax_error_raises_error(self):
|
||||
from application.templates.template_engine import (
|
||||
TemplateEngine,
|
||||
TemplateRenderError,
|
||||
)
|
||||
|
||||
engine = TemplateEngine()
|
||||
|
||||
with pytest.raises(TemplateRenderError, match="Template syntax error"):
|
||||
engine.render("Hello {{ name", {"name": "World"})
|
||||
|
||||
def test_validate_template_valid(self):
|
||||
from application.templates.template_engine import TemplateEngine
|
||||
|
||||
engine = TemplateEngine()
|
||||
assert engine.validate_template("Valid {{ variable }}") is True
|
||||
|
||||
def test_validate_template_invalid(self):
|
||||
from application.templates.template_engine import TemplateEngine
|
||||
|
||||
engine = TemplateEngine()
|
||||
assert engine.validate_template("Invalid {{ variable") is False
|
||||
|
||||
def test_validate_empty_template(self):
|
||||
from application.templates.template_engine import TemplateEngine
|
||||
|
||||
engine = TemplateEngine()
|
||||
assert engine.validate_template("") is True
|
||||
|
||||
def test_extract_variables(self):
|
||||
from application.templates.template_engine import TemplateEngine
|
||||
|
||||
engine = TemplateEngine()
|
||||
template = "{{ user.name }} and {{ user.email }}"
|
||||
|
||||
result = engine.extract_variables(template)
|
||||
|
||||
assert isinstance(result, set)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSystemNamespace:
|
||||
|
||||
def test_system_namespace_build(self):
|
||||
from application.templates.namespaces import SystemNamespace
|
||||
|
||||
builder = SystemNamespace()
|
||||
context = builder.build(
|
||||
request_id="req_123", user_id="user_456", extra_param="ignored"
|
||||
)
|
||||
|
||||
assert context["request_id"] == "req_123"
|
||||
assert context["user_id"] == "user_456"
|
||||
assert "date" in context
|
||||
assert "time" in context
|
||||
assert "timestamp" in context
|
||||
|
||||
def test_system_namespace_generates_request_id(self):
|
||||
from application.templates.namespaces import SystemNamespace
|
||||
|
||||
builder = SystemNamespace()
|
||||
context = builder.build(user_id="user_123")
|
||||
|
||||
assert context["request_id"] is not None
|
||||
assert len(context["request_id"]) > 0
|
||||
|
||||
def test_system_namespace_name(self):
|
||||
from application.templates.namespaces import SystemNamespace
|
||||
|
||||
builder = SystemNamespace()
|
||||
assert builder.namespace_name == "system"
|
||||
|
||||
def test_system_namespace_date_format(self):
|
||||
from application.templates.namespaces import SystemNamespace
|
||||
|
||||
builder = SystemNamespace()
|
||||
context = builder.build()
|
||||
|
||||
import re
|
||||
|
||||
assert re.match(r"\d{4}-\d{2}-\d{2}", context["date"])
|
||||
assert re.match(r"\d{2}:\d{2}:\d{2}", context["time"])
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestPassthroughNamespace:
|
||||
|
||||
def test_passthrough_namespace_build(self):
|
||||
from application.templates.namespaces import PassthroughNamespace
|
||||
|
||||
builder = PassthroughNamespace()
|
||||
passthrough_data = {"company": "Acme", "user_name": "John", "count": 42}
|
||||
|
||||
context = builder.build(passthrough_data=passthrough_data)
|
||||
|
||||
assert context["company"] == "Acme"
|
||||
assert context["user_name"] == "John"
|
||||
assert context["count"] == 42
|
||||
|
||||
def test_passthrough_namespace_empty(self):
|
||||
from application.templates.namespaces import PassthroughNamespace
|
||||
|
||||
builder = PassthroughNamespace()
|
||||
context = builder.build(passthrough_data=None)
|
||||
|
||||
assert context == {}
|
||||
|
||||
def test_passthrough_namespace_filters_unsafe_values(self):
|
||||
from application.templates.namespaces import PassthroughNamespace
|
||||
|
||||
builder = PassthroughNamespace()
|
||||
passthrough_data = {
|
||||
"safe_string": "value",
|
||||
"unsafe_object": {"key": "value"},
|
||||
"safe_bool": True,
|
||||
"unsafe_list": [1, 2, 3],
|
||||
"safe_float": 3.14,
|
||||
}
|
||||
|
||||
context = builder.build(passthrough_data=passthrough_data)
|
||||
|
||||
assert context["safe_string"] == "value"
|
||||
assert context["safe_bool"] is True
|
||||
assert context["safe_float"] == 3.14
|
||||
assert "unsafe_object" not in context
|
||||
assert "unsafe_list" not in context
|
||||
|
||||
def test_passthrough_namespace_allows_none_values(self):
|
||||
from application.templates.namespaces import PassthroughNamespace
|
||||
|
||||
builder = PassthroughNamespace()
|
||||
passthrough_data = {"nullable_field": None}
|
||||
|
||||
context = builder.build(passthrough_data=passthrough_data)
|
||||
|
||||
assert context["nullable_field"] is None
|
||||
|
||||
def test_passthrough_namespace_name(self):
|
||||
from application.templates.namespaces import PassthroughNamespace
|
||||
|
||||
builder = PassthroughNamespace()
|
||||
assert builder.namespace_name == "passthrough"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSourceNamespace:
|
||||
|
||||
def test_source_namespace_build_with_docs(self):
|
||||
from application.templates.namespaces import SourceNamespace
|
||||
|
||||
builder = SourceNamespace()
|
||||
docs = [
|
||||
{"text": "Doc 1", "filename": "file1.txt"},
|
||||
{"text": "Doc 2", "filename": "file2.txt"},
|
||||
]
|
||||
docs_together = "Doc 1 content\n\nDoc 2 content"
|
||||
|
||||
context = builder.build(docs=docs, docs_together=docs_together)
|
||||
|
||||
assert context["documents"] == docs
|
||||
assert context["count"] == 2
|
||||
assert context["content"] == docs_together
|
||||
assert context["summaries"] == docs_together
|
||||
|
||||
def test_source_namespace_build_empty(self):
|
||||
from application.templates.namespaces import SourceNamespace
|
||||
|
||||
builder = SourceNamespace()
|
||||
context = builder.build(docs=None, docs_together=None)
|
||||
|
||||
assert context == {}
|
||||
|
||||
def test_source_namespace_build_docs_only(self):
|
||||
from application.templates.namespaces import SourceNamespace
|
||||
|
||||
builder = SourceNamespace()
|
||||
docs = [{"text": "Doc 1"}]
|
||||
|
||||
context = builder.build(docs=docs)
|
||||
|
||||
assert context["documents"] == docs
|
||||
assert context["count"] == 1
|
||||
assert "content" not in context
|
||||
|
||||
def test_source_namespace_build_docs_together_only(self):
|
||||
from application.templates.namespaces import SourceNamespace
|
||||
|
||||
builder = SourceNamespace()
|
||||
docs_together = "Content here"
|
||||
|
||||
context = builder.build(docs_together=docs_together)
|
||||
|
||||
assert context["content"] == docs_together
|
||||
assert context["summaries"] == docs_together
|
||||
assert "documents" not in context
|
||||
|
||||
def test_source_namespace_name(self):
|
||||
from application.templates.namespaces import SourceNamespace
|
||||
|
||||
builder = SourceNamespace()
|
||||
assert builder.namespace_name == "source"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestToolsNamespace:
|
||||
|
||||
def test_tools_namespace_build_with_memory_data(self):
|
||||
from application.templates.namespaces import ToolsNamespace
|
||||
|
||||
builder = ToolsNamespace()
|
||||
tools_data = {
|
||||
"memory": {"root": "Files:\n- /notes.txt\n- /tasks.txt", "available": True}
|
||||
}
|
||||
|
||||
context = builder.build(tools_data=tools_data)
|
||||
|
||||
assert context["memory"]["root"] == "Files:\n- /notes.txt\n- /tasks.txt"
|
||||
assert context["memory"]["available"] is True
|
||||
|
||||
def test_tools_namespace_build_empty(self):
|
||||
from application.templates.namespaces import ToolsNamespace
|
||||
|
||||
builder = ToolsNamespace()
|
||||
context = builder.build(tools_data=None)
|
||||
|
||||
assert context == {}
|
||||
|
||||
def test_tools_namespace_build_multiple_tools(self):
|
||||
from application.templates.namespaces import ToolsNamespace
|
||||
|
||||
builder = ToolsNamespace()
|
||||
tools_data = {
|
||||
"memory": {"root": "content", "available": True},
|
||||
"search": {"results": ["result1", "result2"]},
|
||||
"api": {"status": "success"},
|
||||
}
|
||||
|
||||
context = builder.build(tools_data=tools_data)
|
||||
|
||||
assert "memory" in context
|
||||
assert "search" in context
|
||||
assert "api" in context
|
||||
assert context["memory"]["root"] == "content"
|
||||
assert context["search"]["results"] == ["result1", "result2"]
|
||||
assert context["api"]["status"] == "success"
|
||||
|
||||
def test_tools_namespace_filters_unsafe_values(self):
|
||||
from application.templates.namespaces import ToolsNamespace
|
||||
|
||||
builder = ToolsNamespace()
|
||||
|
||||
class UnsafeObject:
|
||||
pass
|
||||
|
||||
tools_data = {"safe_tool": {"result": "success"}, "unsafe_tool": UnsafeObject()}
|
||||
|
||||
context = builder.build(tools_data=tools_data)
|
||||
|
||||
assert "safe_tool" in context
|
||||
assert "unsafe_tool" not in context
|
||||
|
||||
def test_tools_namespace_name(self):
|
||||
from application.templates.namespaces import ToolsNamespace
|
||||
|
||||
builder = ToolsNamespace()
|
||||
assert builder.namespace_name == "tools"
|
||||
|
||||
def test_tools_namespace_with_empty_dict(self):
|
||||
from application.templates.namespaces import ToolsNamespace
|
||||
|
||||
builder = ToolsNamespace()
|
||||
context = builder.build(tools_data={})
|
||||
|
||||
assert context == {}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestNamespaceManagerWithTools:
|
||||
|
||||
def test_namespace_manager_includes_tools_in_context(self):
|
||||
from application.templates.namespaces import NamespaceManager
|
||||
|
||||
manager = NamespaceManager()
|
||||
tools_data = {"memory": {"root": "content", "available": True}}
|
||||
|
||||
context = manager.build_context(tools_data=tools_data)
|
||||
|
||||
assert "tools" in context
|
||||
assert context["tools"]["memory"]["root"] == "content"
|
||||
|
||||
def test_namespace_manager_build_context_all_namespaces(self):
|
||||
from application.templates.namespaces import NamespaceManager
|
||||
|
||||
manager = NamespaceManager()
|
||||
context = manager.build_context(
|
||||
request_id="req_123",
|
||||
user_id="user_456",
|
||||
passthrough_data={"key": "value"},
|
||||
docs_together="Document content",
|
||||
tools_data={"memory": {"root": "notes"}},
|
||||
)
|
||||
|
||||
assert "system" in context
|
||||
assert "passthrough" in context
|
||||
assert "source" in context
|
||||
assert "tools" in context
|
||||
assert context["tools"]["memory"]["root"] == "notes"
|
||||
|
||||
def test_namespace_manager_build_context_partial_data(self):
|
||||
from application.templates.namespaces import NamespaceManager
|
||||
|
||||
manager = NamespaceManager()
|
||||
context = manager.build_context(request_id="req_123")
|
||||
|
||||
assert "system" in context
|
||||
assert context["system"]["request_id"] == "req_123"
|
||||
|
||||
def test_namespace_manager_get_builder(self):
|
||||
from application.templates.namespaces import NamespaceManager, SystemNamespace
|
||||
|
||||
manager = NamespaceManager()
|
||||
builder = manager.get_builder("system")
|
||||
|
||||
assert isinstance(builder, SystemNamespace)
|
||||
|
||||
def test_namespace_manager_get_builder_nonexistent(self):
|
||||
from application.templates.namespaces import NamespaceManager
|
||||
|
||||
manager = NamespaceManager()
|
||||
builder = manager.get_builder("nonexistent")
|
||||
|
||||
assert builder is None
|
||||
|
||||
def test_namespace_manager_handles_builder_exceptions(self):
|
||||
from unittest.mock import patch
|
||||
|
||||
from application.templates.namespaces import NamespaceManager
|
||||
|
||||
manager = NamespaceManager()
|
||||
|
||||
with patch.object(
|
||||
manager._builders["system"],
|
||||
"build",
|
||||
side_effect=Exception("Builder error"),
|
||||
):
|
||||
context = manager.build_context()
|
||||
# Namespace should be present but empty when builder fails
|
||||
|
||||
assert "system" in context
|
||||
assert context["system"] == {}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestPromptRenderer:
|
||||
|
||||
def test_render_prompt_with_template_syntax(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
prompt = "Hello {{ system.user_id }}, today is {{ system.date }}"
|
||||
|
||||
result = renderer.render_prompt(prompt, user_id="user_123")
|
||||
|
||||
assert "user_123" in result
|
||||
assert "202" in result
|
||||
|
||||
def test_render_prompt_with_passthrough_data(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
prompt = "Company: {{ passthrough.company }}\nUser: {{ passthrough.user_name }}"
|
||||
passthrough_data = {"company": "Acme", "user_name": "John"}
|
||||
|
||||
result = renderer.render_prompt(prompt, passthrough_data=passthrough_data)
|
||||
|
||||
assert "Company: Acme" in result
|
||||
assert "User: John" in result
|
||||
|
||||
def test_render_prompt_with_source_docs(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
prompt = "Use this information:\n{{ source.content }}"
|
||||
docs_together = "Important document content"
|
||||
|
||||
result = renderer.render_prompt(prompt, docs_together=docs_together)
|
||||
|
||||
assert "Use this information:" in result
|
||||
assert "Important document content" in result
|
||||
|
||||
def test_render_prompt_empty_content(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
result = renderer.render_prompt("")
|
||||
|
||||
assert result == ""
|
||||
|
||||
def test_render_prompt_legacy_format_with_summaries(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
prompt = "Context: {summaries}\nQuestion: What is this?"
|
||||
docs_together = "This is the document content"
|
||||
|
||||
result = renderer.render_prompt(prompt, docs_together=docs_together)
|
||||
|
||||
assert "Context: This is the document content" in result
|
||||
|
||||
def test_render_prompt_legacy_format_without_docs(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
prompt = "Context: {summaries}\nQuestion: What is this?"
|
||||
|
||||
result = renderer.render_prompt(prompt)
|
||||
|
||||
assert "Context: {summaries}" in result
|
||||
|
||||
def test_render_prompt_combined_namespace_variables(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
prompt = "User: {{ passthrough.user }}, Date: {{ system.date }}, Docs: {{ source.content }}"
|
||||
passthrough_data = {"user": "Alice"}
|
||||
docs_together = "Doc content"
|
||||
|
||||
result = renderer.render_prompt(
|
||||
prompt,
|
||||
passthrough_data=passthrough_data,
|
||||
docs_together=docs_together,
|
||||
)
|
||||
|
||||
assert "User: Alice" in result
|
||||
assert "Date: 202" in result
|
||||
assert "Doc content" in result
|
||||
|
||||
def test_render_prompt_with_tools_data(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
prompt = "Memory contents:\n{{ tools.memory.root }}\n\nStatus: {{ tools.memory.available }}"
|
||||
tools_data = {
|
||||
"memory": {"root": "Files:\n- /notes.txt\n- /tasks.txt", "available": True}
|
||||
}
|
||||
|
||||
result = renderer.render_prompt(prompt, tools_data=tools_data)
|
||||
|
||||
assert "Memory contents:" in result
|
||||
assert "Files:" in result
|
||||
assert "/notes.txt" in result
|
||||
assert "/tasks.txt" in result
|
||||
assert "Status: True" in result
|
||||
|
||||
def test_render_prompt_with_all_namespaces(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
prompt = """
|
||||
System: {{ system.date }}
|
||||
User: {{ passthrough.user }}
|
||||
Docs: {{ source.content }}
|
||||
Memory: {{ tools.memory.root }}
|
||||
"""
|
||||
passthrough_data = {"user": "Alice"}
|
||||
docs_together = "Important docs"
|
||||
tools_data = {"memory": {"root": "Notes content", "available": True}}
|
||||
|
||||
result = renderer.render_prompt(
|
||||
prompt,
|
||||
passthrough_data=passthrough_data,
|
||||
docs_together=docs_together,
|
||||
tools_data=tools_data,
|
||||
)
|
||||
|
||||
assert "202" in result
|
||||
assert "Alice" in result
|
||||
assert "Important docs" in result
|
||||
assert "Notes content" in result
|
||||
|
||||
def test_render_prompt_undefined_variable_returns_empty_string(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
prompt = "Hello {{ undefined_var }}"
|
||||
|
||||
result = renderer.render_prompt(prompt)
|
||||
assert result == "Hello "
|
||||
|
||||
def test_render_prompt_with_undefined_variable_in_template(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
prompt = "Hello {{ undefined_name }}"
|
||||
|
||||
result = renderer.render_prompt(prompt)
|
||||
assert result == "Hello "
|
||||
|
||||
def test_validate_template_valid(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
assert renderer.validate_template("Valid {{ variable }}") is True
|
||||
|
||||
def test_validate_template_invalid(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
assert renderer.validate_template("Invalid {{ variable") is False
|
||||
|
||||
def test_extract_variables(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
template = "{{ var1 }} and {{ var2 }}"
|
||||
|
||||
result = renderer.extract_variables(template)
|
||||
|
||||
assert isinstance(result, set)
|
||||
|
||||
def test_uses_template_syntax_detection(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
|
||||
assert renderer._uses_template_syntax("Text with {{ var }}") is True
|
||||
assert renderer._uses_template_syntax("Text with {var}") is False
|
||||
assert renderer._uses_template_syntax("Plain text") is False
|
||||
|
||||
def test_apply_legacy_substitutions(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
prompt = "Use {summaries} to answer"
|
||||
docs_together = "Important info"
|
||||
|
||||
result = renderer._apply_legacy_substitutions(prompt, docs_together)
|
||||
|
||||
assert "Use Important info to answer" in result
|
||||
|
||||
def test_apply_legacy_substitutions_without_docs(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
prompt = "Use {summaries} to answer"
|
||||
|
||||
result = renderer._apply_legacy_substitutions(prompt, None)
|
||||
|
||||
assert result == prompt
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestPromptRendererIntegration:
|
||||
|
||||
def test_render_prompt_real_world_scenario(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
prompt = "You are helping {{ passthrough.company }}.\n\nUser: {{ passthrough.user_name }}\n\nRequest ID: {{ system.request_id }}\n\nDate: {{ system.date }}\n\nReference Documents:\n\n{{ source.content }}\n\nPlease answer the question professionally."
|
||||
|
||||
passthrough_data = {"company": "Tech Corp", "user_name": "Alice"}
|
||||
docs_together = "Document 1: Technical specs\nDocument 2: Requirements"
|
||||
|
||||
result = renderer.render_prompt(
|
||||
prompt,
|
||||
request_id="req_123",
|
||||
user_id="user_456",
|
||||
passthrough_data=passthrough_data,
|
||||
docs_together=docs_together,
|
||||
)
|
||||
|
||||
assert "Tech Corp" in result
|
||||
assert "Alice" in result
|
||||
assert "req_123" in result
|
||||
assert "Technical specs" in result
|
||||
assert "professionally" in result
|
||||
|
||||
def test_render_prompt_multiple_doc_references(self):
|
||||
from application.api.answer.services.prompt_renderer import PromptRenderer
|
||||
|
||||
renderer = PromptRenderer()
|
||||
prompt = """Documents: {{ source.content }} \n\nAlso summaries: {{ source.summaries }}"""
|
||||
docs_together = "Content here"
|
||||
|
||||
result = renderer.render_prompt(prompt, docs_together=docs_together)
|
||||
|
||||
assert result.count("Content here") == 2
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestStreamProcessorPromptRendering:
|
||||
|
||||
def test_stream_processor_pre_fetch_docs_none_doc_mode(self, mock_mongo_db):
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
|
||||
request_data = {"question": "Test question", "isNoneDoc": True}
|
||||
processor = StreamProcessor(request_data, None)
|
||||
|
||||
docs_together, docs_list = processor.pre_fetch_docs("Test question")
|
||||
|
||||
assert docs_together is None
|
||||
assert docs_list is None
|
||||
|
||||
def test_pre_fetch_tools_disabled_globally(self, mock_mongo_db, monkeypatch):
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
from application.core.settings import settings
|
||||
|
||||
monkeypatch.setattr(settings, "ENABLE_TOOL_PREFETCH", False)
|
||||
|
||||
request_data = {"question": "test"}
|
||||
processor = StreamProcessor(request_data, {"sub": "user1"})
|
||||
|
||||
result = processor.pre_fetch_tools()
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_pre_fetch_tools_disabled_per_request(self, mock_mongo_db):
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
|
||||
request_data = {"question": "test", "disable_tool_prefetch": True}
|
||||
processor = StreamProcessor(request_data, {"sub": "user1"})
|
||||
|
||||
result = processor.pre_fetch_tools()
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_pre_fetch_tools_skips_tool_with_no_actions(self, mock_mongo_db):
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
from application.core.mongo_db import MongoDB
|
||||
from bson import ObjectId
|
||||
|
||||
db = MongoDB.get_client()[list(MongoDB.get_client().keys())[0]]
|
||||
tool_doc = {
|
||||
"_id": ObjectId(),
|
||||
"name": "memory",
|
||||
"user": "user1",
|
||||
"status": True,
|
||||
"config": {},
|
||||
}
|
||||
db["user_tools"].insert_one(tool_doc)
|
||||
|
||||
request_data = {"question": "test"}
|
||||
processor = StreamProcessor(request_data, {"sub": "user1"})
|
||||
|
||||
with patch(
|
||||
"application.agents.tools.tool_manager.ToolManager"
|
||||
) as mock_manager_class:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
|
||||
# Mock the tool instance
|
||||
mock_tool = MagicMock()
|
||||
mock_manager.load_tool.return_value = mock_tool
|
||||
|
||||
# Tool has no actions
|
||||
mock_tool.get_actions_metadata.return_value = []
|
||||
|
||||
result = processor.pre_fetch_tools()
|
||||
|
||||
# Should return None when tool has no actions
|
||||
assert result is None
|
||||
|
||||
def test_pre_fetch_tools_enabled_by_default(self, mock_mongo_db, monkeypatch):
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
from application.core.mongo_db import MongoDB
|
||||
from bson import ObjectId
|
||||
|
||||
db = MongoDB.get_client()[list(MongoDB.get_client().keys())[0]]
|
||||
tool_doc = {
|
||||
"_id": ObjectId(),
|
||||
"name": "memory",
|
||||
"user": "user1",
|
||||
"status": True,
|
||||
"config": {},
|
||||
}
|
||||
db["user_tools"].insert_one(tool_doc)
|
||||
|
||||
request_data = {"question": "test"}
|
||||
processor = StreamProcessor(request_data, {"sub": "user1"})
|
||||
|
||||
with patch(
|
||||
"application.agents.tools.tool_manager.ToolManager"
|
||||
) as mock_manager_class:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
|
||||
# Mock the tool instance returned by load_tool
|
||||
mock_tool = MagicMock()
|
||||
mock_manager.load_tool.return_value = mock_tool
|
||||
|
||||
# Mock get_actions_metadata on the tool instance
|
||||
mock_tool.get_actions_metadata.return_value = [
|
||||
{"name": "memory_ls", "description": "List files", "parameters": {"properties": {}}}
|
||||
]
|
||||
mock_tool.execute_action.return_value = "Directory: /\n- file.txt"
|
||||
|
||||
result = processor.pre_fetch_tools()
|
||||
|
||||
assert result is not None
|
||||
assert "memory" in result
|
||||
assert "memory_ls" in result["memory"]
|
||||
|
||||
def test_pre_fetch_tools_no_tools_configured(self, mock_mongo_db):
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
|
||||
request_data = {"question": "test"}
|
||||
processor = StreamProcessor(request_data, {"sub": "user1"})
|
||||
|
||||
result = processor.pre_fetch_tools()
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_pre_fetch_tools_memory_returns_error(self, mock_mongo_db):
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
from application.core.mongo_db import MongoDB
|
||||
from bson import ObjectId
|
||||
|
||||
db = MongoDB.get_client()[list(MongoDB.get_client().keys())[0]]
|
||||
tool_doc = {
|
||||
"_id": ObjectId(),
|
||||
"name": "memory",
|
||||
"user": "user1",
|
||||
"status": True,
|
||||
"config": {},
|
||||
}
|
||||
db["user_tools"].insert_one(tool_doc)
|
||||
|
||||
request_data = {"question": "test"}
|
||||
processor = StreamProcessor(request_data, {"sub": "user1"})
|
||||
|
||||
with patch(
|
||||
"application.agents.tools.tool_manager.ToolManager"
|
||||
) as mock_manager_class:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
|
||||
# Mock the tool instance
|
||||
mock_tool = MagicMock()
|
||||
mock_manager.load_tool.return_value = mock_tool
|
||||
|
||||
mock_tool.get_actions_metadata.return_value = [
|
||||
{"name": "memory_ls", "description": "List files", "parameters": {"properties": {}}}
|
||||
]
|
||||
# Simulate execution error
|
||||
mock_tool.execute_action.side_effect = Exception("Tool error")
|
||||
|
||||
result = processor.pre_fetch_tools()
|
||||
|
||||
# Should return None when all actions fail
|
||||
assert result is None
|
||||
|
||||
def test_pre_fetch_tools_memory_returns_empty(self, mock_mongo_db):
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
from application.core.mongo_db import MongoDB
|
||||
from bson import ObjectId
|
||||
|
||||
db = MongoDB.get_client()[list(MongoDB.get_client().keys())[0]]
|
||||
tool_doc = {
|
||||
"_id": ObjectId(),
|
||||
"name": "memory",
|
||||
"user": "user1",
|
||||
"status": True,
|
||||
"config": {},
|
||||
}
|
||||
db["user_tools"].insert_one(tool_doc)
|
||||
|
||||
request_data = {"question": "test"}
|
||||
processor = StreamProcessor(request_data, {"sub": "user1"})
|
||||
|
||||
with patch(
|
||||
"application.agents.tools.tool_manager.ToolManager"
|
||||
) as mock_manager_class:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
|
||||
# Mock the tool instance
|
||||
mock_tool = MagicMock()
|
||||
mock_manager.load_tool.return_value = mock_tool
|
||||
|
||||
mock_tool.get_actions_metadata.return_value = [
|
||||
{"name": "memory_ls", "description": "List files", "parameters": {"properties": {}}}
|
||||
]
|
||||
# Return empty string
|
||||
mock_tool.execute_action.return_value = ""
|
||||
|
||||
result = processor.pre_fetch_tools()
|
||||
|
||||
# Empty result should still be included
|
||||
assert result is not None
|
||||
assert "memory" in result
|
||||
@@ -250,3 +250,330 @@ class TestStreamProcessorAttachments:
|
||||
"attachments" not in processor.data
|
||||
or processor.data.get("attachments") is None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestToolPreFetch:
|
||||
"""Tests for tool pre-fetching with saved parameter values from MongoDB"""
|
||||
|
||||
def test_cryptoprice_prefetch_with_saved_parameters(self, mock_mongo_db):
|
||||
"""Test that cryptoprice tool is pre-fetched with saved parameter values from MongoDB structure"""
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
from application.core.settings import settings
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# Setup MongoDB with cryptoprice tool configuration
|
||||
# NOTE: The collection is called "user_tools" not "tools"
|
||||
tools_collection = mock_mongo_db[settings.MONGO_DB_NAME]["user_tools"]
|
||||
tool_id = ObjectId()
|
||||
|
||||
tools_collection.insert_one(
|
||||
{
|
||||
"_id": tool_id,
|
||||
"name": "cryptoprice",
|
||||
"user": "user_123",
|
||||
"status": True, # Must be True for tool to be included
|
||||
"actions": [
|
||||
{
|
||||
"name": "cryptoprice_get",
|
||||
"description": "Get cryptocurrency price",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"symbol": {
|
||||
"type": "string",
|
||||
"description": "Crypto symbol",
|
||||
"value": "BTC" # Saved value in MongoDB
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"description": "Currency for price",
|
||||
"value": "USD" # Saved value in MongoDB
|
||||
}
|
||||
},
|
||||
"required": ["symbol", "currency"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
"token": ""
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
request_data = {
|
||||
"question": "What is the price of Bitcoin?",
|
||||
"tools": [str(tool_id)]
|
||||
}
|
||||
|
||||
processor = StreamProcessor(request_data, {"sub": "user_123"})
|
||||
processor._required_tool_actions = {"cryptoprice": {"cryptoprice_get"}}
|
||||
|
||||
# Mock the ToolManager and tool instance
|
||||
with patch("application.agents.tools.tool_manager.ToolManager") as mock_manager_class:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
|
||||
# Mock the tool instance returned by load_tool
|
||||
mock_tool = MagicMock()
|
||||
mock_manager.load_tool.return_value = mock_tool
|
||||
|
||||
# Mock get_actions_metadata on the tool instance
|
||||
mock_tool.get_actions_metadata.return_value = [
|
||||
{
|
||||
"name": "cryptoprice_get",
|
||||
"description": "Get cryptocurrency price",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"symbol": {"type": "string", "description": "Crypto symbol"},
|
||||
"currency": {"type": "string", "description": "Currency for price"}
|
||||
},
|
||||
"required": ["symbol", "currency"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# Mock execute_action on the tool instance to return price data
|
||||
mock_tool.execute_action.return_value = {
|
||||
"status_code": 200,
|
||||
"price": 45000.50,
|
||||
"message": "Price of BTC in USD retrieved successfully."
|
||||
}
|
||||
|
||||
# Execute pre-fetch
|
||||
tools_data = processor.pre_fetch_tools()
|
||||
|
||||
# Verify the tool was called
|
||||
assert mock_tool.execute_action.called
|
||||
|
||||
# Verify it was called with the saved parameters from MongoDB
|
||||
call_args = mock_tool.execute_action.call_args
|
||||
assert call_args is not None
|
||||
|
||||
# Check action name uses the full metadata name for execution
|
||||
assert call_args[0][0] == "cryptoprice_get"
|
||||
|
||||
# Check kwargs contain saved values
|
||||
kwargs = call_args[1]
|
||||
assert kwargs.get("symbol") == "BTC"
|
||||
assert kwargs.get("currency") == "USD"
|
||||
|
||||
# Verify tools_data structure
|
||||
assert "cryptoprice" in tools_data
|
||||
# Results are exposed under the full action name
|
||||
assert "cryptoprice_get" in tools_data["cryptoprice"]
|
||||
assert tools_data["cryptoprice"]["cryptoprice_get"]["price"] == 45000.50
|
||||
|
||||
def test_prefetch_with_missing_saved_values_uses_defaults(self, mock_mongo_db):
|
||||
"""Test that pre-fetch falls back to defaults when saved values are missing"""
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
from application.core.settings import settings
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
tools_collection = mock_mongo_db[settings.MONGO_DB_NAME]["user_tools"]
|
||||
tool_id = ObjectId()
|
||||
|
||||
# Tool configuration without saved values
|
||||
tools_collection.insert_one(
|
||||
{
|
||||
"_id": tool_id,
|
||||
"name": "cryptoprice",
|
||||
"user": "user_123",
|
||||
"status": True,
|
||||
"actions": [
|
||||
{
|
||||
"name": "cryptoprice_get",
|
||||
"description": "Get cryptocurrency price",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"symbol": {
|
||||
"type": "string",
|
||||
"description": "Crypto symbol",
|
||||
"default": "ETH" # Only default, no saved value
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"description": "Currency",
|
||||
"default": "EUR"
|
||||
}
|
||||
},
|
||||
"required": ["symbol", "currency"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"config": {}
|
||||
}
|
||||
)
|
||||
|
||||
request_data = {
|
||||
"question": "Crypto price?",
|
||||
"tools": [str(tool_id)]
|
||||
}
|
||||
|
||||
processor = StreamProcessor(request_data, {"sub": "user_123"})
|
||||
processor._required_tool_actions = {"cryptoprice": {"cryptoprice_get"}}
|
||||
|
||||
with patch("application.agents.tools.tool_manager.ToolManager") as mock_manager_class:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
|
||||
# Mock the tool instance
|
||||
mock_tool = MagicMock()
|
||||
mock_manager.load_tool.return_value = mock_tool
|
||||
|
||||
mock_tool.get_actions_metadata.return_value = [
|
||||
{
|
||||
"name": "cryptoprice_get",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"symbol": {"type": "string", "default": "ETH"},
|
||||
"currency": {"type": "string", "default": "EUR"}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
mock_tool.execute_action.return_value = {
|
||||
"status_code": 200,
|
||||
"price": 2500.00
|
||||
}
|
||||
|
||||
processor.pre_fetch_tools()
|
||||
|
||||
# Should use default values when saved values are missing
|
||||
call_args = mock_tool.execute_action.call_args
|
||||
if call_args:
|
||||
kwargs = call_args[1]
|
||||
# Either uses defaults or skips if no values available
|
||||
assert kwargs.get("symbol") in ["ETH", None]
|
||||
assert kwargs.get("currency") in ["EUR", None]
|
||||
|
||||
def test_prefetch_with_tool_id_reference(self, mock_mongo_db):
|
||||
"""Test that tools can be referenced by MongoDB ObjectId in templates"""
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
from application.core.settings import settings
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
tools_collection = mock_mongo_db[settings.MONGO_DB_NAME]["user_tools"]
|
||||
tool_id = ObjectId()
|
||||
|
||||
# Create a tool in the database
|
||||
tools_collection.insert_one(
|
||||
{
|
||||
"_id": tool_id,
|
||||
"name": "memory",
|
||||
"user": "user_123",
|
||||
"status": True,
|
||||
"actions": [
|
||||
{
|
||||
"name": "memory_ls",
|
||||
"description": "List files",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
],
|
||||
"config": {},
|
||||
}
|
||||
)
|
||||
|
||||
request_data = {"question": "test"}
|
||||
processor = StreamProcessor(request_data, {"sub": "user_123"})
|
||||
|
||||
# Mock the filtering to require this specific tool by ID
|
||||
processor._required_tool_actions = {
|
||||
str(tool_id): {"memory_ls"} # Reference by ObjectId string
|
||||
}
|
||||
|
||||
with patch(
|
||||
"application.agents.tools.tool_manager.ToolManager"
|
||||
) as mock_manager_class:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
|
||||
# Mock the tool instance
|
||||
mock_tool = MagicMock()
|
||||
mock_manager.load_tool.return_value = mock_tool
|
||||
|
||||
mock_tool.get_actions_metadata.return_value = [
|
||||
{"name": "memory_ls", "description": "List files", "parameters": {"properties": {}}}
|
||||
]
|
||||
mock_tool.execute_action.return_value = "Directory: /\n- file.txt"
|
||||
|
||||
result = processor.pre_fetch_tools()
|
||||
|
||||
# Tool data should be available under both name and ID
|
||||
assert result is not None
|
||||
assert "memory" in result
|
||||
assert str(tool_id) in result
|
||||
# Both should point to the same data
|
||||
assert result["memory"] == result[str(tool_id)]
|
||||
assert "memory_ls" in result[str(tool_id)]
|
||||
|
||||
def test_prefetch_with_multiple_same_name_tools(self, mock_mongo_db):
|
||||
"""Test that multiple tools with the same name can be distinguished by ID"""
|
||||
from application.api.answer.services.stream_processor import StreamProcessor
|
||||
from application.core.settings import settings
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
tools_collection = mock_mongo_db[settings.MONGO_DB_NAME]["user_tools"]
|
||||
|
||||
# Create two memory tools with different IDs
|
||||
tool_id_1 = ObjectId()
|
||||
tool_id_2 = ObjectId()
|
||||
|
||||
tools_collection.insert_many([
|
||||
{
|
||||
"_id": tool_id_1,
|
||||
"name": "memory",
|
||||
"user": "user_123",
|
||||
"status": True,
|
||||
"actions": [{"name": "memory_ls", "parameters": {"properties": {}}}],
|
||||
"config": {"path": "/home"},
|
||||
},
|
||||
{
|
||||
"_id": tool_id_2,
|
||||
"name": "memory",
|
||||
"user": "user_123",
|
||||
"status": True,
|
||||
"actions": [{"name": "memory_ls", "parameters": {"properties": {}}}],
|
||||
"config": {"path": "/work"},
|
||||
}
|
||||
])
|
||||
|
||||
request_data = {"question": "test"}
|
||||
processor = StreamProcessor(request_data, {"sub": "user_123"})
|
||||
|
||||
# Mock the filtering to require only the second tool by ID
|
||||
processor._required_tool_actions = {
|
||||
str(tool_id_2): {"memory_ls"} # Only reference the second one
|
||||
}
|
||||
|
||||
with patch(
|
||||
"application.agents.tools.tool_manager.ToolManager"
|
||||
) as mock_manager_class:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
|
||||
# Mock the tool instance
|
||||
mock_tool = MagicMock()
|
||||
mock_manager.load_tool.return_value = mock_tool
|
||||
|
||||
mock_tool.get_actions_metadata.return_value = [
|
||||
{"name": "memory_ls", "parameters": {"properties": {}}}
|
||||
]
|
||||
mock_tool.execute_action.return_value = "Work directory"
|
||||
|
||||
result = processor.pre_fetch_tools()
|
||||
|
||||
# Only the second tool should be fetched (referenced by ID)
|
||||
assert result is not None
|
||||
assert str(tool_id_2) in result
|
||||
# Since filtering is enabled and only tool_id_2 is referenced,
|
||||
# only tool_id_2 should be pre-fetched
|
||||
# The "memory" key will still exist because we store under both name and ID
|
||||
assert "memory" in result
|
||||
|
||||
138
tests/parser/file/test_embedding_pipeline.py
Normal file
138
tests/parser/file/test_embedding_pipeline.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import pytest
|
||||
import logging
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from application.parser.embedding_pipeline import (
|
||||
sanitize_content,
|
||||
add_text_to_store_with_retry,
|
||||
embed_and_store_documents,
|
||||
)
|
||||
|
||||
|
||||
|
||||
def test_sanitize_content_removes_nulls():
|
||||
content = "This\x00is\x00a\x00test"
|
||||
result = sanitize_content(content)
|
||||
assert "\x00" not in result
|
||||
assert result == "Thisisatest"
|
||||
|
||||
|
||||
def test_sanitize_content_empty_or_none():
|
||||
assert sanitize_content("") == ""
|
||||
assert sanitize_content(None) is None
|
||||
|
||||
|
||||
|
||||
def test_add_text_to_store_with_retry_success():
|
||||
store = MagicMock()
|
||||
doc = MagicMock()
|
||||
doc.page_content = "Test content"
|
||||
doc.metadata = {}
|
||||
|
||||
add_text_to_store_with_retry(store, doc, "123")
|
||||
|
||||
store.add_texts.assert_called_once_with(
|
||||
["Test content"], metadatas=[{"source_id": "123"}]
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_settings(monkeypatch):
|
||||
mock_settings = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"application.parser.embedding_pipeline.settings", mock_settings
|
||||
)
|
||||
return mock_settings
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_vector_creator(monkeypatch):
|
||||
mock_creator = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"application.parser.embedding_pipeline.VectorCreator", mock_creator
|
||||
)
|
||||
return mock_creator
|
||||
|
||||
|
||||
|
||||
def test_embed_and_store_documents_creates_folder(tmp_path, mock_settings, mock_vector_creator):
|
||||
mock_settings.VECTOR_STORE = "faiss"
|
||||
|
||||
docs = [MagicMock(page_content="doc1", metadata={}), MagicMock(page_content="doc2", metadata={})]
|
||||
folder_name = tmp_path / "test_store"
|
||||
source_id = "xyz"
|
||||
task_status = MagicMock()
|
||||
|
||||
mock_store = MagicMock()
|
||||
mock_vector_creator.create_vectorstore.return_value = mock_store
|
||||
|
||||
embed_and_store_documents(docs, str(folder_name), source_id, task_status)
|
||||
|
||||
assert folder_name.exists()
|
||||
mock_vector_creator.create_vectorstore.assert_called_once()
|
||||
mock_store.save_local.assert_called_once_with(str(folder_name))
|
||||
task_status.update_state.assert_called()
|
||||
|
||||
|
||||
def test_embed_and_store_documents_non_faiss(tmp_path, mock_settings, mock_vector_creator):
|
||||
mock_settings.VECTOR_STORE = "chromadb"
|
||||
|
||||
docs = [MagicMock(page_content="doc1", metadata={}), MagicMock(page_content="doc2", metadata={})]
|
||||
folder_name = tmp_path / "chromadb_store"
|
||||
source_id = "test123"
|
||||
task_status = MagicMock()
|
||||
|
||||
mock_store = MagicMock()
|
||||
mock_vector_creator.create_vectorstore.return_value = mock_store
|
||||
|
||||
embed_and_store_documents(docs, str(folder_name), source_id, task_status)
|
||||
|
||||
mock_store.delete_index.assert_called_once()
|
||||
task_status.update_state.assert_called()
|
||||
assert folder_name.exists()
|
||||
|
||||
|
||||
@patch("application.parser.embedding_pipeline.add_text_to_store_with_retry")
|
||||
def test_embed_and_store_documents_partial_failure(
|
||||
mock_add_retry, tmp_path, mock_settings, mock_vector_creator, caplog
|
||||
):
|
||||
mock_settings.VECTOR_STORE = "faiss"
|
||||
|
||||
docs = [MagicMock(page_content="good", metadata={}), MagicMock(page_content="bad", metadata={})]
|
||||
folder_name = tmp_path / "partial_fail"
|
||||
source_id = "id123"
|
||||
task_status = MagicMock()
|
||||
|
||||
mock_store = MagicMock()
|
||||
mock_vector_creator.create_vectorstore.return_value = mock_store
|
||||
|
||||
# First document succeeds, second fails
|
||||
def side_effect(*args, **kwargs):
|
||||
if "bad" in args[1].page_content:
|
||||
raise Exception("Embedding failed")
|
||||
mock_add_retry.side_effect = side_effect
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
embed_and_store_documents(docs, str(folder_name), source_id, task_status)
|
||||
|
||||
assert "Error embedding document" in caplog.text
|
||||
mock_store.save_local.assert_called()
|
||||
|
||||
|
||||
def test_embed_and_store_documents_save_fails_raises_oserror(
|
||||
tmp_path, mock_settings, mock_vector_creator
|
||||
):
|
||||
mock_settings.VECTOR_STORE = "faiss"
|
||||
|
||||
docs = [MagicMock(page_content="good", metadata={})]
|
||||
folder_name = tmp_path / "save_fail"
|
||||
source_id = "id789"
|
||||
task_status = MagicMock()
|
||||
|
||||
mock_store = MagicMock()
|
||||
mock_store.save_local.side_effect = Exception("Disk full")
|
||||
mock_vector_creator.create_vectorstore.return_value = mock_store
|
||||
|
||||
with pytest.raises(OSError, match="Unable to save vector store"):
|
||||
embed_and_store_documents(docs, str(folder_name), source_id, task_status)
|
||||
|
||||
@@ -29,15 +29,15 @@ class FakeCollection:
|
||||
pass
|
||||
|
||||
def insert_one(self, doc):
|
||||
key = (doc["user_id"], doc["tool_id"], int(doc["todo_id"]))
|
||||
key = (doc["user_id"], doc["tool_id"], doc["todo_id"])
|
||||
self.docs[key] = doc
|
||||
return type("res", (), {"inserted_id": key})
|
||||
|
||||
def find_one(self, query):
|
||||
key = (query.get("user_id"), query.get("tool_id"), int(query.get("todo_id")))
|
||||
key = (query.get("user_id"), query.get("tool_id"), query.get("todo_id"))
|
||||
return self.docs.get(key)
|
||||
|
||||
def find(self, query):
|
||||
def find(self, query, projection=None):
|
||||
user_id = query.get("user_id")
|
||||
tool_id = query.get("tool_id")
|
||||
filtered = [
|
||||
@@ -47,7 +47,7 @@ class FakeCollection:
|
||||
return FakeCursor(filtered)
|
||||
|
||||
def update_one(self, query, update, upsert=False):
|
||||
key = (query.get("user_id"), query.get("tool_id"), int(query.get("todo_id")))
|
||||
key = (query.get("user_id"), query.get("tool_id"), query.get("todo_id"))
|
||||
if key in self.docs:
|
||||
self.docs[key].update(update.get("$set", {}))
|
||||
return type("res", (), {"matched_count": 1})
|
||||
@@ -59,7 +59,7 @@ class FakeCollection:
|
||||
return type("res", (), {"matched_count": 0})
|
||||
|
||||
def delete_one(self, query):
|
||||
key = (query.get("user_id"), query.get("tool_id"), int(query.get("todo_id")))
|
||||
key = (query.get("user_id"), query.get("tool_id"), query.get("todo_id"))
|
||||
if key in self.docs:
|
||||
del self.docs[key]
|
||||
return type("res", (), {"deleted_count": 1})
|
||||
@@ -69,6 +69,10 @@ class FakeCollection:
|
||||
@pytest.fixture
|
||||
def todo_tool(monkeypatch) -> TodoListTool:
|
||||
"""Provides a TodoListTool with a fake MongoDB backend."""
|
||||
# Reset the MongoDB client cache to ensure our mock is used
|
||||
from application.core.mongo_db import MongoDB
|
||||
MongoDB._client = None
|
||||
|
||||
fake_collection = FakeCollection()
|
||||
fake_client = {settings.MONGO_DB_NAME: {"todos": fake_collection}}
|
||||
monkeypatch.setattr("application.core.mongo_db.MongoDB.get_client", lambda: fake_client)
|
||||
@@ -76,52 +80,72 @@ def todo_tool(monkeypatch) -> TodoListTool:
|
||||
|
||||
|
||||
def test_create_and_get(todo_tool: TodoListTool):
|
||||
res = todo_tool.execute_action("todo_create", title="Write tests", description="Write pytest cases")
|
||||
assert res["status_code"] == 201
|
||||
todo_id = res["todo_id"]
|
||||
res = todo_tool.execute_action("create", title="Write tests")
|
||||
assert "Todo created with ID" in res
|
||||
# Extract todo_id from response like "Todo created with ID test_user_test_tool_1: Write tests"
|
||||
todo_id = res.split("ID ")[1].split(":")[0].strip()
|
||||
|
||||
get_res = todo_tool.execute_action("todo_get", todo_id=todo_id)
|
||||
assert get_res["status_code"] == 200
|
||||
assert get_res["todo"]["title"] == "Write tests"
|
||||
assert get_res["todo"]["description"] == "Write pytest cases"
|
||||
get_res = todo_tool.execute_action("get", todo_id=todo_id)
|
||||
assert "Error" not in get_res
|
||||
assert "Write tests" in get_res
|
||||
|
||||
|
||||
def test_get_all_todos(todo_tool: TodoListTool):
|
||||
todo_tool.execute_action("todo_create", title="Task 1")
|
||||
todo_tool.execute_action("todo_create", title="Task 2")
|
||||
todo_tool.execute_action("create", title="Task 1")
|
||||
todo_tool.execute_action("create", title="Task 2")
|
||||
|
||||
list_res = todo_tool.execute_action("todo_list")
|
||||
assert list_res["status_code"] == 200
|
||||
titles = [todo["title"] for todo in list_res["todos"]]
|
||||
assert "Task 1" in titles
|
||||
assert "Task 2" in titles
|
||||
list_res = todo_tool.execute_action("list")
|
||||
assert "Task 1" in list_res
|
||||
assert "Task 2" in list_res
|
||||
|
||||
|
||||
def test_update_todo(todo_tool: TodoListTool):
|
||||
create_res = todo_tool.execute_action("todo_create", title="Initial Title")
|
||||
todo_id = create_res["todo_id"]
|
||||
create_res = todo_tool.execute_action("create", title="Initial Title")
|
||||
todo_id = create_res.split("ID ")[1].split(":")[0].strip()
|
||||
|
||||
update_res = todo_tool.execute_action("todo_update", todo_id=todo_id, updates={"title": "Updated Title", "status": "done"})
|
||||
assert update_res["status_code"] == 200
|
||||
update_res = todo_tool.execute_action("update", todo_id=todo_id, title="Updated Title")
|
||||
assert "updated" in update_res.lower()
|
||||
assert "Updated Title" in update_res
|
||||
|
||||
get_res = todo_tool.execute_action("todo_get", todo_id=todo_id)
|
||||
assert get_res["todo"]["title"] == "Updated Title"
|
||||
assert get_res["todo"]["status"] == "done"
|
||||
get_res = todo_tool.execute_action("get", todo_id=todo_id)
|
||||
assert "Updated Title" in get_res
|
||||
|
||||
|
||||
def test_complete_todo(todo_tool: TodoListTool):
|
||||
create_res = todo_tool.execute_action("create", title="To Complete")
|
||||
todo_id = create_res.split("ID ")[1].split(":")[0].strip()
|
||||
|
||||
# Check initial status is open
|
||||
get_res = todo_tool.execute_action("get", todo_id=todo_id)
|
||||
assert "open" in get_res
|
||||
|
||||
# Mark as completed
|
||||
complete_res = todo_tool.execute_action("complete", todo_id=todo_id)
|
||||
assert "completed" in complete_res.lower()
|
||||
|
||||
# Verify status changed to completed
|
||||
get_res = todo_tool.execute_action("get", todo_id=todo_id)
|
||||
assert "completed" in get_res
|
||||
|
||||
|
||||
def test_delete_todo(todo_tool: TodoListTool):
|
||||
create_res = todo_tool.execute_action("todo_create", title="To Delete")
|
||||
todo_id = create_res["todo_id"]
|
||||
create_res = todo_tool.execute_action("create", title="To Delete")
|
||||
todo_id = create_res.split("ID ")[1].split(":")[0].strip()
|
||||
|
||||
delete_res = todo_tool.execute_action("todo_delete", todo_id=todo_id)
|
||||
assert delete_res["status_code"] == 200
|
||||
delete_res = todo_tool.execute_action("delete", todo_id=todo_id)
|
||||
assert "deleted" in delete_res.lower()
|
||||
|
||||
get_res = todo_tool.execute_action("todo_get", todo_id=todo_id)
|
||||
assert get_res["status_code"] == 404
|
||||
get_res = todo_tool.execute_action("get", todo_id=todo_id)
|
||||
assert "Error" in get_res
|
||||
assert "not found" in get_res
|
||||
|
||||
|
||||
def test_isolation_and_default_tool_id(monkeypatch):
|
||||
"""Ensure todos are isolated by tool_id and user_id."""
|
||||
# Reset the MongoDB client cache to ensure our mock is used
|
||||
from application.core.mongo_db import MongoDB
|
||||
MongoDB._client = None
|
||||
|
||||
fake_collection = FakeCollection()
|
||||
fake_client = {settings.MONGO_DB_NAME: {"todos": fake_collection}}
|
||||
monkeypatch.setattr("application.core.mongo_db.MongoDB.get_client", lambda: fake_client)
|
||||
@@ -130,17 +154,20 @@ def test_isolation_and_default_tool_id(monkeypatch):
|
||||
tool1 = TodoListTool({"tool_id": "tool_1"}, user_id="u1")
|
||||
tool2 = TodoListTool({"tool_id": "tool_2"}, user_id="u1")
|
||||
|
||||
r1_create = tool1.execute_action("todo_create", title="from tool 1")
|
||||
r2_create = tool2.execute_action("todo_create", title="from tool 2")
|
||||
r1_create = tool1.execute_action("create", title="from tool 1")
|
||||
r2_create = tool2.execute_action("create", title="from tool 2")
|
||||
|
||||
r1 = tool1.execute_action("todo_get", todo_id=r1_create["todo_id"])
|
||||
r2 = tool2.execute_action("todo_get", todo_id=r2_create["todo_id"])
|
||||
todo_id_1 = r1_create.split("ID ")[1].split(":")[0].strip()
|
||||
todo_id_2 = r2_create.split("ID ")[1].split(":")[0].strip()
|
||||
|
||||
assert r1["status_code"] == 200
|
||||
assert r1["todo"]["title"] == "from tool 1"
|
||||
r1 = tool1.execute_action("get", todo_id=todo_id_1)
|
||||
r2 = tool2.execute_action("get", todo_id=todo_id_2)
|
||||
|
||||
assert r2["status_code"] == 200
|
||||
assert r2["todo"]["title"] == "from tool 2"
|
||||
assert "Error" not in r1
|
||||
assert "from tool 1" in r1
|
||||
|
||||
assert "Error" not in r2
|
||||
assert "from tool 2" in r2
|
||||
|
||||
# Same user, no tool_id → should default to same value
|
||||
t3 = TodoListTool({}, user_id="default_user")
|
||||
@@ -149,8 +176,9 @@ def test_isolation_and_default_tool_id(monkeypatch):
|
||||
assert t3.tool_id == "default_default_user"
|
||||
assert t4.tool_id == "default_default_user"
|
||||
|
||||
create_res = t3.execute_action("todo_create", title="shared default")
|
||||
r = t4.execute_action("todo_get", todo_id=create_res["todo_id"])
|
||||
create_res = t3.execute_action("create", title="shared default")
|
||||
todo_id = create_res.split("ID ")[1].split(":")[0].strip()
|
||||
r = t4.execute_action("get", todo_id=todo_id)
|
||||
|
||||
assert r["status_code"] == 200
|
||||
assert r["todo"]["title"] == "shared default"
|
||||
assert "Error" not in r
|
||||
assert "shared default" in r
|
||||
|
||||
Reference in New Issue
Block a user