Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa20cc9a98 | |||
| c7edaf3e84 | |||
| e6ba1fcd82 | |||
| 4d65d20f01 | |||
| ff67366c5c | |||
| d34f48819d | |||
| 75ad3e0dc6 | |||
| dbb24d7d14 |
@@ -12,6 +12,12 @@ AGENT_RECURSION_LIMIT=30
|
|||||||
# Example: ALLOWED_ORIGINS=http://localhost:3000,http://example.com
|
# Example: ALLOWED_ORIGINS=http://localhost:3000,http://example.com
|
||||||
ALLOWED_ORIGINS=http://localhost:3000
|
ALLOWED_ORIGINS=http://localhost:3000
|
||||||
|
|
||||||
|
# Enable or disable MCP server configuration, the default is false.
|
||||||
|
# Please enable this feature before securing your front-end and back-end in a managed environment.
|
||||||
|
|
||||||
|
# Otherwise, you system could be compromised.
|
||||||
|
ENABLE_MCP_SERVER_CONFIGURATION=false
|
||||||
|
|
||||||
# Search Engine, Supported values: tavily (recommended), duckduckgo, brave_search, arxiv
|
# Search Engine, Supported values: tavily (recommended), duckduckgo, brave_search, arxiv
|
||||||
SEARCH_API=tavily
|
SEARCH_API=tavily
|
||||||
TAVILY_API_KEY=tvly-xxx
|
TAVILY_API_KEY=tvly-xxx
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
# MCP Integrations
|
# MCP Integrations(Beta)
|
||||||
|
|
||||||
|
Now This feature is diabled by default. You can enable it by setting the environment ENABLE_MCP_SERVER_CONFIGURATION to be true
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Please enable this feature before securing your frond-end and back-end in a managed environment.
|
||||||
|
> Otherwise, you system could be compromised.
|
||||||
|
|
||||||
|
This feature is diabled by default. You can enable it by setting the environment ENABLE_MCP_SERVER_CONFIGURATION
|
||||||
|
Please enable this feature before securing your frond-end and back-end in an internal environment.q
|
||||||
|
|
||||||
## Example of MCP Server Configuration
|
## Example of MCP Server Configuration
|
||||||
|
|
||||||
|
|||||||
+8
-11
@@ -243,16 +243,12 @@ def coordinator_node(
|
|||||||
"Coordinator response contains no tool calls. Terminating workflow execution."
|
"Coordinator response contains no tool calls. Terminating workflow execution."
|
||||||
)
|
)
|
||||||
logger.debug(f"Coordinator response: {response}")
|
logger.debug(f"Coordinator response: {response}")
|
||||||
old_messages = state.get("messages", [])
|
messages = state.get("messages", [])
|
||||||
new_messages = old_messages + [
|
if response.content:
|
||||||
{
|
messages.append(HumanMessage(content=response.content, name="coordinator"))
|
||||||
"role": "assistant",
|
|
||||||
"content": response.content,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
return Command(
|
return Command(
|
||||||
update={
|
update={
|
||||||
"messages": new_messages,
|
"messages": messages,
|
||||||
"locale": locale,
|
"locale": locale,
|
||||||
"research_topic": research_topic,
|
"research_topic": research_topic,
|
||||||
"resources": configurable.resources,
|
"resources": configurable.resources,
|
||||||
@@ -311,6 +307,7 @@ async def _execute_agent_step(
|
|||||||
) -> Command[Literal["research_team"]]:
|
) -> Command[Literal["research_team"]]:
|
||||||
"""Helper function to execute a step using the specified agent."""
|
"""Helper function to execute a step using the specified agent."""
|
||||||
current_plan = state.get("current_plan")
|
current_plan = state.get("current_plan")
|
||||||
|
plan_title = current_plan.title
|
||||||
observations = state.get("observations", [])
|
observations = state.get("observations", [])
|
||||||
|
|
||||||
# Find the first unexecuted step
|
# Find the first unexecuted step
|
||||||
@@ -332,16 +329,16 @@ async def _execute_agent_step(
|
|||||||
# Format completed steps information
|
# Format completed steps information
|
||||||
completed_steps_info = ""
|
completed_steps_info = ""
|
||||||
if completed_steps:
|
if completed_steps:
|
||||||
completed_steps_info = "# Existing Research Findings\n\n"
|
completed_steps_info = "# Completed Research Steps\n\n"
|
||||||
for i, step in enumerate(completed_steps):
|
for i, step in enumerate(completed_steps):
|
||||||
completed_steps_info += f"## Existing Finding {i + 1}: {step.title}\n\n"
|
completed_steps_info += f"## Completed Step {i + 1}: {step.title}\n\n"
|
||||||
completed_steps_info += f"<finding>\n{step.execution_res}\n</finding>\n\n"
|
completed_steps_info += f"<finding>\n{step.execution_res}\n</finding>\n\n"
|
||||||
|
|
||||||
# Prepare the input for the agent with completed steps info
|
# Prepare the input for the agent with completed steps info
|
||||||
agent_input = {
|
agent_input = {
|
||||||
"messages": [
|
"messages": [
|
||||||
HumanMessage(
|
HumanMessage(
|
||||||
content=f"{completed_steps_info}# Current Task\n\n## Title\n\n{current_step.title}\n\n## Description\n\n{current_step.description}\n\n## Locale\n\n{state.get('locale', 'en-US')}"
|
content=f"# Research Topic\n\n{plan_title}\n\n{completed_steps_info}# Current Step\n\n## Title\n\n{current_step.title}\n\n## Description\n\n{current_step.description}\n\n## Locale\n\n{state.get('locale', 'en-US')}"
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-3
@@ -58,12 +58,14 @@ app = FastAPI(
|
|||||||
allowed_origins_str = os.getenv("ALLOWED_ORIGINS", "http://localhost:3000")
|
allowed_origins_str = os.getenv("ALLOWED_ORIGINS", "http://localhost:3000")
|
||||||
allowed_origins = [origin.strip() for origin in allowed_origins_str.split(",")]
|
allowed_origins = [origin.strip() for origin in allowed_origins_str.split(",")]
|
||||||
|
|
||||||
|
logger.info(f"Allowed origins: {allowed_origins}")
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=allowed_origins, # Restrict to specific origins
|
allow_origins=allowed_origins, # Restrict to specific origins
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["GET", "POST"], # Be specific about allowed methods
|
allow_methods=["GET", "POST", "OPTIONS"], # Use the configured list of methods
|
||||||
allow_headers=["Content-Type", "Authorization", "X-Requested-With"], # Be specific
|
allow_headers=["*"], # Now allow all headers, but can be restricted further
|
||||||
)
|
)
|
||||||
|
|
||||||
graph = build_graph_with_memory()
|
graph = build_graph_with_memory()
|
||||||
@@ -71,6 +73,20 @@ graph = build_graph_with_memory()
|
|||||||
|
|
||||||
@app.post("/api/chat/stream")
|
@app.post("/api/chat/stream")
|
||||||
async def chat_stream(request: ChatRequest):
|
async def chat_stream(request: ChatRequest):
|
||||||
|
# Check if MCP server configuration is enabled
|
||||||
|
mcp_enabled = os.getenv("ENABLE_MCP_SERVER_CONFIGURATION", "false").lower() in [
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Validate MCP settings if provided
|
||||||
|
if request.mcp_settings and not mcp_enabled:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="MCP server configuration is disabled. Set ENABLE_MCP_SERVER_CONFIGURATION=true to enable MCP features.",
|
||||||
|
)
|
||||||
|
|
||||||
thread_id = request.thread_id
|
thread_id = request.thread_id
|
||||||
if thread_id == "__default__":
|
if thread_id == "__default__":
|
||||||
thread_id = str(uuid4())
|
thread_id = str(uuid4())
|
||||||
@@ -84,7 +100,7 @@ async def chat_stream(request: ChatRequest):
|
|||||||
request.max_search_results,
|
request.max_search_results,
|
||||||
request.auto_accepted_plan,
|
request.auto_accepted_plan,
|
||||||
request.interrupt_feedback,
|
request.interrupt_feedback,
|
||||||
request.mcp_settings,
|
request.mcp_settings if mcp_enabled else {},
|
||||||
request.enable_background_investigation,
|
request.enable_background_investigation,
|
||||||
request.report_style,
|
request.report_style,
|
||||||
request.enable_deep_thinking,
|
request.enable_deep_thinking,
|
||||||
@@ -363,6 +379,17 @@ async def enhance_prompt(request: EnhancePromptRequest):
|
|||||||
@app.post("/api/mcp/server/metadata", response_model=MCPServerMetadataResponse)
|
@app.post("/api/mcp/server/metadata", response_model=MCPServerMetadataResponse)
|
||||||
async def mcp_server_metadata(request: MCPServerMetadataRequest):
|
async def mcp_server_metadata(request: MCPServerMetadataRequest):
|
||||||
"""Get information about an MCP server."""
|
"""Get information about an MCP server."""
|
||||||
|
# Check if MCP server configuration is enabled
|
||||||
|
if os.getenv("ENABLE_MCP_SERVER_CONFIGURATION", "false").lower() not in [
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="MCP server configuration is disabled. Set ENABLE_MCP_SERVER_CONFIGURATION=true to enable MCP features.",
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Set default timeout with a longer value for this endpoint
|
# Set default timeout with a longer value for this endpoint
|
||||||
timeout = 300 # Default to 300 seconds for this endpoint
|
timeout = 300 # Default to 300 seconds for this endpoint
|
||||||
|
|||||||
@@ -260,6 +260,10 @@ class TestEnhancePromptEndpoint:
|
|||||||
|
|
||||||
class TestMCPEndpoint:
|
class TestMCPEndpoint:
|
||||||
@patch("src.server.app.load_mcp_tools")
|
@patch("src.server.app.load_mcp_tools")
|
||||||
|
@patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"ENABLE_MCP_SERVER_CONFIGURATION": "true"},
|
||||||
|
)
|
||||||
def test_mcp_server_metadata_success(self, mock_load_tools, client):
|
def test_mcp_server_metadata_success(self, mock_load_tools, client):
|
||||||
mock_load_tools.return_value = [
|
mock_load_tools.return_value = [
|
||||||
{"name": "test_tool", "description": "Test tool"}
|
{"name": "test_tool", "description": "Test tool"}
|
||||||
@@ -281,6 +285,10 @@ class TestMCPEndpoint:
|
|||||||
assert len(response_data["tools"]) == 1
|
assert len(response_data["tools"]) == 1
|
||||||
|
|
||||||
@patch("src.server.app.load_mcp_tools")
|
@patch("src.server.app.load_mcp_tools")
|
||||||
|
@patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"ENABLE_MCP_SERVER_CONFIGURATION": "true"},
|
||||||
|
)
|
||||||
def test_mcp_server_metadata_with_custom_timeout(self, mock_load_tools, client):
|
def test_mcp_server_metadata_with_custom_timeout(self, mock_load_tools, client):
|
||||||
mock_load_tools.return_value = []
|
mock_load_tools.return_value = []
|
||||||
|
|
||||||
@@ -296,6 +304,10 @@ class TestMCPEndpoint:
|
|||||||
mock_load_tools.assert_called_once()
|
mock_load_tools.assert_called_once()
|
||||||
|
|
||||||
@patch("src.server.app.load_mcp_tools")
|
@patch("src.server.app.load_mcp_tools")
|
||||||
|
@patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"ENABLE_MCP_SERVER_CONFIGURATION": "true"},
|
||||||
|
)
|
||||||
def test_mcp_server_metadata_with_exception(self, mock_load_tools, client):
|
def test_mcp_server_metadata_with_exception(self, mock_load_tools, client):
|
||||||
mock_load_tools.side_effect = HTTPException(
|
mock_load_tools.side_effect = HTTPException(
|
||||||
status_code=400, detail="MCP Server Error"
|
status_code=400, detail="MCP Server Error"
|
||||||
@@ -313,6 +325,30 @@ class TestMCPEndpoint:
|
|||||||
assert response.status_code == 500
|
assert response.status_code == 500
|
||||||
assert response.json()["detail"] == "Internal Server Error"
|
assert response.json()["detail"] == "Internal Server Error"
|
||||||
|
|
||||||
|
@patch("src.server.app.load_mcp_tools")
|
||||||
|
@patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"ENABLE_MCP_SERVER_CONFIGURATION": ""},
|
||||||
|
)
|
||||||
|
def test_mcp_server_metadata_without_enable_configuration(
|
||||||
|
self, mock_load_tools, client
|
||||||
|
):
|
||||||
|
|
||||||
|
request_data = {
|
||||||
|
"transport": "stdio",
|
||||||
|
"command": "test_command",
|
||||||
|
"args": ["arg1", "arg2"],
|
||||||
|
"env": {"ENV_VAR": "value"},
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post("/api/mcp/server/metadata", json=request_data)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert (
|
||||||
|
response.json()["detail"]
|
||||||
|
== "MCP server configuration is disabled. Set ENABLE_MCP_SERVER_CONFIGURATION=true to enable MCP features."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestRAGEndpoints:
|
class TestRAGEndpoints:
|
||||||
@patch("src.server.app.SELECTED_RAG_PROVIDER", "test_provider")
|
@patch("src.server.app.SELECTED_RAG_PROVIDER", "test_provider")
|
||||||
@@ -377,6 +413,89 @@ class TestChatStreamEndpoint:
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.headers["content-type"] == "text/event-stream; charset=utf-8"
|
assert response.headers["content-type"] == "text/event-stream; charset=utf-8"
|
||||||
|
|
||||||
|
@patch("src.server.app.graph")
|
||||||
|
def test_chat_stream_with_mcp_settings(self, mock_graph, client):
|
||||||
|
# Mock the async stream
|
||||||
|
async def mock_astream(*args, **kwargs):
|
||||||
|
yield ("agent1", "step1", {"test": "data"})
|
||||||
|
|
||||||
|
mock_graph.astream = mock_astream
|
||||||
|
|
||||||
|
request_data = {
|
||||||
|
"thread_id": "__default__",
|
||||||
|
"messages": [{"role": "user", "content": "Hello"}],
|
||||||
|
"resources": [],
|
||||||
|
"max_plan_iterations": 3,
|
||||||
|
"max_step_num": 10,
|
||||||
|
"max_search_results": 5,
|
||||||
|
"auto_accepted_plan": True,
|
||||||
|
"interrupt_feedback": "",
|
||||||
|
"mcp_settings": {
|
||||||
|
"servers": {
|
||||||
|
"mcp-github-trending": {
|
||||||
|
"transport": "stdio",
|
||||||
|
"command": "uvx",
|
||||||
|
"args": ["mcp-github-trending"],
|
||||||
|
"env": {"MCP_SERVER_ID": "mcp-github-trending"},
|
||||||
|
"enabled_tools": ["get_github_trending_repositories"],
|
||||||
|
"add_to_agents": ["researcher"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enable_background_investigation": False,
|
||||||
|
"report_style": "academic",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post("/api/chat/stream", json=request_data)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert (
|
||||||
|
response.json()["detail"]
|
||||||
|
== "MCP server configuration is disabled. Set ENABLE_MCP_SERVER_CONFIGURATION=true to enable MCP features."
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("src.server.app.graph")
|
||||||
|
@patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"ENABLE_MCP_SERVER_CONFIGURATION": "true"},
|
||||||
|
)
|
||||||
|
def test_chat_stream_with_mcp_settings_enabled(self, mock_graph, client):
|
||||||
|
# Mock the async stream
|
||||||
|
async def mock_astream(*args, **kwargs):
|
||||||
|
yield ("agent1", "step1", {"test": "data"})
|
||||||
|
|
||||||
|
mock_graph.astream = mock_astream
|
||||||
|
|
||||||
|
request_data = {
|
||||||
|
"thread_id": "__default__",
|
||||||
|
"messages": [{"role": "user", "content": "Hello"}],
|
||||||
|
"resources": [],
|
||||||
|
"max_plan_iterations": 3,
|
||||||
|
"max_step_num": 10,
|
||||||
|
"max_search_results": 5,
|
||||||
|
"auto_accepted_plan": True,
|
||||||
|
"interrupt_feedback": "",
|
||||||
|
"mcp_settings": {
|
||||||
|
"servers": {
|
||||||
|
"mcp-github-trending": {
|
||||||
|
"transport": "stdio",
|
||||||
|
"command": "uvx",
|
||||||
|
"args": ["mcp-github-trending"],
|
||||||
|
"env": {"MCP_SERVER_ID": "mcp-github-trending"},
|
||||||
|
"enabled_tools": ["get_github_trending_repositories"],
|
||||||
|
"add_to_agents": ["researcher"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enable_background_investigation": False,
|
||||||
|
"report_style": "academic",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post("/api/chat/stream", json=request_data)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.headers["content-type"] == "text/event-stream; charset=utf-8"
|
||||||
|
|
||||||
|
|
||||||
class TestAstreamWorkflowGenerator:
|
class TestAstreamWorkflowGenerator:
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
+28294
-385
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useStore, useToolCalls } from "~/core/store";
|
import { useStore, useToolCalls } from "~/core/store";
|
||||||
|
import { parseJSON } from "~/core/utils/json";
|
||||||
import { Tooltip } from "./tooltip";
|
import { Tooltip } from "./tooltip";
|
||||||
import { WarningFilled } from "@ant-design/icons";
|
import { WarningFilled } from "@ant-design/icons";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -23,7 +24,7 @@ export const Link = ({
|
|||||||
(toolCalls || []).forEach((call) => {
|
(toolCalls || []).forEach((call) => {
|
||||||
if (call && call.name === "web_search" && call.result) {
|
if (call && call.name === "web_search" && call.result) {
|
||||||
try {
|
try {
|
||||||
const result = JSON.parse(call.result) as Array<{ url: string }>;
|
const result = parseJSON(call.result, []) as Array<{ url: string }>;
|
||||||
if (Array.isArray(result)) {
|
if (Array.isArray(result)) {
|
||||||
result.forEach((r) => {
|
result.forEach((r) => {
|
||||||
if (r && typeof r.url === 'string') {
|
if (r && typeof r.url === 'string') {
|
||||||
|
|||||||
Reference in New Issue
Block a user