Compare commits

...

15 Commits

Author SHA1 Message Date
xing.liu d0c6f450d4 feat(ut): add ut coverage check 2025-05-15 23:21:55 +08:00
Leo Hui a43db94fb6 feat: refactor crawler trust link style (#166)
* feat: refactor crawler trust link style

* feat: enhance link credibility checks in Markdown and related components
2025-05-15 17:17:10 +08:00
JeffJiang 8802eea0ba fix: report editor styles (#163)
* fix: report editor styles
2025-05-15 15:18:01 +08:00
Leo Hui 1a59accb52 fix: adjust slider width for responsive design in multi-agent visualization (#134) 2025-05-15 11:59:16 +08:00
JeffJiang 86295ed195 fix: hallucination link warn (#158) 2025-05-15 10:58:24 +08:00
JeffJiang bf4820c68f Check the output links are hallucinations from AI (#139)
* feat: check output links if a hallucination from AI
2025-05-15 10:39:53 +08:00
Abeautifulsnow 25e7b86f02 optimize docker backend image size (#130) 2025-05-15 09:52:14 +08:00
Maxim Kot 0459e3c9f8 Update README.md (#122)
Fixed the link to the configuration from the docker section
2025-05-15 08:54:33 +08:00
DanielWalnut 5cc0e61297 refactor: refine the step execute human message (#144) 2025-05-14 18:54:14 +08:00
Henry Li a220f4b6ea feat: add python result and error handling (#141) 2025-05-14 03:47:28 -07:00
DanielWalnut f73a7a229c refactor: add existing research findings into step human message (#140) 2025-05-14 18:40:14 +08:00
laundry d983149984 fix:rollback to fix the update version error (#136)
* fix: fix package error

Change-Id: I0f7a962656df7e45da03b591296fbf3afc398b64

* fix: rollback uv.lock

Change-Id: I465849c0d3d7a4d0757ecd79ff156feab1217f70
2025-05-14 17:06:53 +08:00
laundry 3d5e579ebd fix: fix start error when search engine is not tavliy and env TAVILY_API_KEY not exist (#133)
Change-Id: I58e865a11e89acaa3c0b884578cd995d0e9b5422
2025-05-14 14:45:36 +08:00
Leo Hui a14ca92c36 refactor: extract link and image components for Markdown rendering (#119) 2025-05-14 10:45:34 +08:00
XingLiu0923 b85a7592dc feat(trace): add langsmith tracing (#126) 2025-05-14 10:12:50 +08:00
24 changed files with 469 additions and 54 deletions
+6
View File
@@ -17,5 +17,11 @@ VOLCENGINE_TTS_ACCESS_TOKEN=xxx
# VOLCENGINE_TTS_CLUSTER=volcano_tts # Optional, default is volcano_tts
# VOLCENGINE_TTS_VOICE_TYPE=BV700_V2_streaming # Optional, default is BV700_V2_streaming
# Option, for langsmith tracing and monitoring
# LANGSMITH_TRACING=true
# LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
# LANGSMITH_API_KEY="xxx"
# LANGSMITH_PROJECT="xxx"
# [!NOTE]
# For model settings and other configurations, please refer to `docs/configuration_guide.md`
+18 -2
View File
@@ -23,7 +23,23 @@ jobs:
uv pip install -e ".[dev]"
uv pip install -e ".[test]"
- name: Run test cases
- name: Run test cases with coverage
run: |
source .venv/bin/activate
TAVILY_API_KEY=mock-key make test
TAVILY_API_KEY=mock-key make coverage
- name: Generate HTML Coverage Report
run: |
source .venv/bin/activate
python -m coverage html -d coverage_html
- name: Upload Coverage Report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage_html/
- name: Display Coverage Summary
run: |
source .venv/bin/activate
python -m coverage report
+3
View File
@@ -21,3 +21,6 @@ conf.yaml
.idea/
.langgraph_api/
# coverage report
coverage.xml
coverage/
+1 -1
View File
@@ -1,4 +1,4 @@
FROM ghcr.io/astral-sh/uv:python3.12-bookworm
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
# Install uv.
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
+1 -1
View File
@@ -19,4 +19,4 @@ langgraph-dev:
uvx --refresh --from "langgraph-cli[inmem]" --with-editable . --python 3.12 langgraph dev --allow-blocking
coverage:
uv run pytest --cov=src tests/ --cov-report=term-missing
uv run pytest --cov=src tests/ --cov-report=term-missing --cov-report=xml
+21 -2
View File
@@ -2,7 +2,7 @@
[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![DeepWiki](https://img.shields.io/badge/DeepWiki-bytedance%2Fdeer--flow-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/bytedance/deer-flow)
[![DeepWiki](https://img.shields.io/badge/DeepWiki-bytedance%2Fdeer--flow-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McCcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/bytedance/deer-flow)
<!-- DeepWiki badge generated by https://deepwiki.ryoppippi.com/ -->
@@ -347,11 +347,30 @@ When you submit a research topic in the Studio UI, you'll be able to see the ent
- The research and writing phases for each section
- The final report generation
### Enabling LangSmith Tracing
DeerFlow supports LangSmith tracing to help you debug and monitor your workflows. To enable LangSmith tracing:
1. Make sure your `.env` file has the following configurations (see `.env.example`):
```bash
LANGSMITH_TRACING=true
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
LANGSMITH_API_KEY="xxx"
LANGSMITH_PROJECT="xxx"
```
2. Start tracing and visualize the graph locally with LangSmith by running:
```bash
langgraph dev
```
This will enable trace visualization in LangGraph Studio and send your traces to LangSmith for monitoring and analysis.
## Docker
You can also run this project with Docker.
First, you need read the [configuration](#configuration) below. Make sure `.env`, `.conf.yaml` files are ready.
First, you need read the [configuration](docs/configuration_guide.md) below. Make sure `.env`, `.conf.yaml` files are ready.
Second, to build a Docker image of your own web server:
+19
View File
@@ -333,6 +333,25 @@ Wenn Sie ein Forschungsthema in der Studio UI einreichen, können Sie die gesamt
- Die Forschungs- und Schreibphasen für jeden Abschnitt
- Die Erstellung des endgültigen Berichts
### Aktivieren von LangSmith-Tracing
DeerFlow unterstützt LangSmith-Tracing, um Ihnen beim Debuggen und Überwachen Ihrer Workflows zu helfen. Um LangSmith-Tracing zu aktivieren:
1. Stellen Sie sicher, dass Ihre `.env`-Datei die folgenden Konfigurationen enthält (siehe `.env.example`):
```bash
LANGSMITH_TRACING=true
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
LANGSMITH_API_KEY="xxx"
LANGSMITH_PROJECT="xxx"
```
2. Starten Sie das Tracing mit LangSmith lokal, indem Sie folgenden Befehl ausführen:
```bash
langgraph dev
```
Dies aktiviert die Trace-Visualisierung in LangGraph Studio und sendet Ihre Traces zur Überwachung und Analyse an LangSmith.
## Beispiele
Die folgenden Beispiele demonstrieren die Fähigkeiten von DeerFlow:
+19
View File
@@ -322,6 +322,25 @@ Studio UI で研究トピックを送信すると、次を含む全ワークフ
- 各セクションの研究と執筆段階
- 最終レポート生成
### LangSmith トレースの有効化
DeerFlow は LangSmith トレース機能をサポートしており、ワークフローのデバッグとモニタリングに役立ちます。LangSmith トレースを有効にするには:
1. `.env` ファイルに次の設定があることを確認してください(`.env.example` を参照):
```bash
LANGSMITH_TRACING=true
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
LANGSMITH_API_KEY="xxx"
LANGSMITH_PROJECT="xxx"
```
2. 次のコマンドを実行して LangSmith トレースを開始します:
```bash
langgraph dev
```
これにより、LangGraph Studio でトレース可視化が有効になり、トレースがモニタリングと分析のために LangSmith に送信されます。
## Docker
このプロジェクトは Docker でも実行できます。
+19
View File
@@ -322,6 +322,25 @@ langgraph dev
- 每个部分的研究和写作阶段
- 最终报告生成
### 启用 LangSmith 追踪
DeerFlow 支持 LangSmith 追踪功能,帮助您调试和监控工作流。要启用 LangSmith 追踪:
1. 确保您的 `.env` 文件中有以下配置(参见 `.env.example`):
```bash
LANGSMITH_TRACING=true
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
LANGSMITH_API_KEY="xxx"
LANGSMITH_PROJECT="xxx"
```
2. 通过运行以下命令本地启动 LangSmith 追踪:
```bash
langgraph dev
```
这将在 LangGraph Studio 中启用追踪可视化,并将您的追踪发送到 LangSmith 进行监控和分析。
## Docker
您也可以使用 Docker 运行此项目。
+3
View File
@@ -52,6 +52,9 @@ filterwarnings = [
"ignore::UserWarning",
]
[tool.coverage.report]
fail_under = 25
[tool.hatch.build.targets.wheel]
packages = ["src"]
+22 -5
View File
@@ -308,17 +308,34 @@ async def _execute_agent_step(
observations = state.get("observations", [])
# Find the first unexecuted step
current_step = None
completed_steps = []
for step in current_plan.steps:
if not step.execution_res:
current_step = step
break
else:
completed_steps.append(step)
logger.info(f"Executing step: {step.title}")
if not current_step:
logger.warning("No unexecuted step found")
return Command(goto="research_team")
# Prepare the input for the agent
logger.info(f"Executing step: {current_step.title}")
# Format completed steps information
completed_steps_info = ""
if completed_steps:
completed_steps_info = "# Existing Research Findings\n\n"
for i, step in enumerate(completed_steps):
completed_steps_info += f"## Existing Finding {i+1}: {step.title}\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
agent_input = {
"messages": [
HumanMessage(
content=f"#Task\n\n##title\n\n{step.title}\n\n##description\n\n{step.description}\n\n##locale\n\n{state.get('locale', 'en-US')}"
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')}"
)
]
}
@@ -340,8 +357,8 @@ async def _execute_agent_step(
logger.debug(f"{agent_name.capitalize()} full response: {response_content}")
# Update the step with the execution result
step.execution_res = response_content
logger.info(f"Step '{step.title}' execution completed by {agent_name}")
current_step.execution_res = response_content
logger.info(f"Step '{current_step.title}' execution completed by {agent_name}")
return Command(
update={
+11 -8
View File
@@ -9,7 +9,7 @@ from langchain_community.tools import BraveSearch, DuckDuckGoSearchResults
from langchain_community.tools.arxiv import ArxivQueryRun
from langchain_community.utilities import ArxivAPIWrapper, BraveSearchWrapper
from src.config import SEARCH_MAX_RESULTS
from src.config import SEARCH_MAX_RESULTS, SearchEngine
from src.tools.tavily_search.tavily_search_results_with_images import (
TavilySearchResultsWithImages,
)
@@ -19,13 +19,16 @@ from src.tools.decorators import create_logged_tool
logger = logging.getLogger(__name__)
LoggedTavilySearch = create_logged_tool(TavilySearchResultsWithImages)
tavily_search_tool = LoggedTavilySearch(
name="web_search",
max_results=SEARCH_MAX_RESULTS,
include_raw_content=True,
include_images=True,
include_image_descriptions=True,
)
if os.getenv("SEARCH_API", "") == SearchEngine.TAVILY.value:
tavily_search_tool = LoggedTavilySearch(
name="web_search",
max_results=SEARCH_MAX_RESULTS,
include_raw_content=True,
include_images=True,
include_image_descriptions=True,
)
else:
tavily_search_tool = None
LoggedDuckDuckGoSearch = create_logged_tool(DuckDuckGoSearchResults)
duckduckgo_search_tool = LoggedDuckDuckGoSearch(
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
"""
This script manually patches sys.modules to fix the LLM import issue
so that tests can run without requiring LLM configuration.
"""
import sys
from unittest.mock import MagicMock
# Create mocks
mock_llm = MagicMock()
mock_llm.invoke.return_value = "Mock LLM response"
# Create a mock module for llm.py
mock_llm_module = MagicMock()
mock_llm_module.get_llm_by_type = lambda llm_type: mock_llm
mock_llm_module.basic_llm = mock_llm
mock_llm_module._create_llm_use_conf = lambda llm_type, conf: mock_llm
# Set the mock module
sys.modules["src.llms.llm"] = mock_llm_module
print("Successfully patched LLM module. You can now run your tests.")
print("Example: uv run pytest tests/test_types.py -v")
+131
View File
@@ -0,0 +1,131 @@
import pytest
import sys
import os
from typing import Annotated, List, Optional
# Import MessagesState directly from langgraph rather than through our application
from langgraph.graph import MessagesState
# Create stub versions of Plan/Step/StepType to avoid dependencies
class StepType:
RESEARCH = "research"
PROCESSING = "processing"
class Step:
def __init__(self, need_web_search, title, description, step_type):
self.need_web_search = need_web_search
self.title = title
self.description = description
self.step_type = step_type
class Plan:
def __init__(self, locale, has_enough_context, thought, title, steps):
self.locale = locale
self.has_enough_context = has_enough_context
self.thought = thought
self.title = title
self.steps = steps
# Import the actual State class by loading the module directly
# This avoids the cascade of imports that would normally happen
def load_state_class():
# Get the absolute path to the types.py file
src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))
types_path = os.path.join(src_dir, "graph", "types.py")
# Create a namespace for the module
import types
module_name = "src.graph.types_direct"
spec = types.ModuleType(module_name)
# Add the module to sys.modules to avoid import loops
sys.modules[module_name] = spec
# Set up the namespace with required imports
spec.__dict__["operator"] = __import__("operator")
spec.__dict__["Annotated"] = Annotated
spec.__dict__["MessagesState"] = MessagesState
spec.__dict__["Plan"] = Plan
# Execute the module code
with open(types_path, "r") as f:
module_code = f.read()
exec(module_code, spec.__dict__)
# Return the State class
return spec.State
# Load the actual State class
State = load_state_class()
def test_state_initialization():
"""Test that State class has correct default attribute definitions."""
# Test that the class has the expected attribute definitions
assert State.locale == "en-US"
assert State.observations == []
assert State.plan_iterations == 0
assert State.current_plan is None
assert State.final_report == ""
assert State.auto_accepted_plan is False
assert State.enable_background_investigation is True
assert State.background_investigation_results is None
# Verify state initialization
state = State(messages=[])
assert "messages" in state
# Without explicitly passing attributes, they're not in the state
assert "locale" not in state
assert "observations" not in state
def test_state_with_custom_values():
"""Test that State can be initialized with custom values."""
test_step = Step(
need_web_search=True,
title="Test Step",
description="Step description",
step_type=StepType.RESEARCH,
)
test_plan = Plan(
locale="en-US",
has_enough_context=False,
thought="Test thought",
title="Test Plan",
steps=[test_step],
)
# Initialize state with custom values and required messages field
state = State(
messages=[],
locale="fr-FR",
observations=["Observation 1"],
plan_iterations=2,
current_plan=test_plan,
final_report="Test report",
auto_accepted_plan=True,
enable_background_investigation=False,
background_investigation_results="Test results",
)
# Access state keys - these are explicitly initialized
assert state["locale"] == "fr-FR"
assert state["observations"] == ["Observation 1"]
assert state["plan_iterations"] == 2
assert state["current_plan"].title == "Test Plan"
assert state["current_plan"].thought == "Test thought"
assert len(state["current_plan"].steps) == 1
assert state["current_plan"].steps[0].title == "Test Step"
assert state["final_report"] == "Test report"
assert state["auto_accepted_plan"] is True
assert state["enable_background_investigation"] is False
assert state["background_investigation_results"] == "Test results"
@@ -75,7 +75,9 @@ function ActivityMessage({ messageId }: { messageId: string }) {
if (message.agent !== "reporter" && message.agent !== "planner") {
return (
<div className="px-4 py-2">
<Markdown animated>{message.content}</Markdown>
<Markdown animated checkLinkCredibility>
{message.content}
</Markdown>
</div>
);
}
@@ -304,10 +306,58 @@ function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
</SyntaxHighlighter>
</div>
</div>
{toolCall.result && <PythonToolCallResult result={toolCall.result} />}
</section>
);
}
function PythonToolCallResult({ result }: { result: string }) {
const { resolvedTheme } = useTheme();
const hasError = useMemo(
() => result.includes("Error executing code:\n"),
[result],
);
const error = useMemo(() => {
if (hasError) {
const parts = result.split("```\nError: ");
if (parts.length > 1) {
return parts[1]!.trim();
}
}
return null;
}, [result, hasError]);
const stdout = useMemo(() => {
if (!hasError) {
const parts = result.split("```\nStdout: ");
if (parts.length > 1) {
return parts[1]!.trim();
}
}
return null;
}, [result, hasError]);
return (
<>
<div className="mt-4 font-medium italic">
{hasError ? "Error when executing the above code" : "Execution output"}
</div>
<div className="bg-accent mt-2 max-h-[400px] max-w-[calc(100%-120px)] overflow-y-auto rounded-md p-2 text-sm">
<SyntaxHighlighter
language="plaintext"
style={resolvedTheme === "dark" ? dark : docco}
customStyle={{
color: hasError ? "red" : "inherit",
background: "transparent",
border: "none",
boxShadow: "none",
}}
>
{error ?? stdout ?? "(empty)"}
</SyntaxHighlighter>
</div>
</>
);
}
function MCPToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
const tool = useMemo(() => findMCPTool(toolCall.name), [toolCall.name]);
const { resolvedTheme } = useTheme();
+18 -1
View File
@@ -1,7 +1,7 @@
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
// SPDX-License-Identifier: MIT
import { Check, Copy, Headphones, X } from "lucide-react";
import { Check, Copy, Headphones, Pencil, Undo2, X } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { ScrollContainer } from "~/components/deer-flow/scroll-container";
@@ -47,6 +47,7 @@ export function ResearchBlock({
await listenToPodcast(researchId);
}, [researchId]);
const [editing, setEditing] = useState(false);
const [copied, setCopied] = useState(false);
const handleCopy = useCallback(() => {
if (!reportId) {
@@ -63,6 +64,10 @@ export function ResearchBlock({
}, 1000);
}, [reportId]);
const handleEdit = useCallback(() => {
setEditing((editing) => !editing);
}, []);
// When the research id changes, set the active tab to activities
useEffect(() => {
if (!hasReport) {
@@ -87,6 +92,17 @@ export function ResearchBlock({
<Headphones />
</Button>
</Tooltip>
<Tooltip title="Edit">
<Button
className="text-gray-400"
size="icon"
variant="ghost"
disabled={isReplay}
onClick={handleEdit}
>
{editing ? <Undo2 /> : <Pencil />}
</Button>
</Tooltip>
<Tooltip title="Copy">
<Button
className="text-gray-400"
@@ -147,6 +163,7 @@ export function ResearchBlock({
className="mt-4"
researchId={researchId}
messageId={reportId}
editing={editing}
/>
)}
</ScrollContainer>
@@ -13,10 +13,12 @@ import { cn } from "~/lib/utils";
export function ResearchReportBlock({
className,
messageId,
editing,
}: {
className?: string;
researchId: string;
messageId: string;
editing: boolean;
}) {
const message = useMessage(messageId);
const { isReplay } = useReplay();
@@ -55,14 +57,16 @@ export function ResearchReportBlock({
ref={contentRef}
className={cn("relative flex flex-col pt-4 pb-8", className)}
>
{!isReplay && isCompleted ? (
{!isReplay && isCompleted && editing ? (
<ReportEditor
content={message?.content}
onMarkdownChange={handleMarkdownChange}
/>
) : (
<>
<Markdown animated>{message?.content}</Markdown>
<Markdown animated checkLinkCredibility>
{message?.content}
</Markdown>
{message?.isStreaming && <LoadingAnimation className="my-12" />}
</>
)}
@@ -147,7 +147,7 @@ export function MultiAgentVisualization({ className }: { className?: string }) {
</Tooltip>
<div className="text-muted-foreground ml-2 flex items-center justify-center">
<Slider
className="w-120"
className="w-40 sm:w-80 md:w-100 lg:w-120"
max={playbook.steps.length - 1}
min={0}
step={1}
+54
View File
@@ -0,0 +1,54 @@
import { useMemo } from "react";
import { useStore, useToolCalls } from "~/core/store";
import { Tooltip } from "./tooltip";
import { WarningFilled } from "@ant-design/icons";
export const Link = ({
href,
children,
checkLinkCredibility = false,
}: {
href: string | undefined;
children: React.ReactNode;
checkLinkCredibility: boolean;
}) => {
const toolCalls = useToolCalls();
const responding = useStore((state) => state.responding);
const credibleLinks = useMemo(() => {
const links = new Set<string>();
if (!checkLinkCredibility) return links;
(toolCalls || []).forEach((call) => {
if (call && call.name === "web_search" && call.result) {
const result = JSON.parse(call.result) as Array<{ url: string }>;
result.forEach((r) => {
links.add(r.url);
});
}
});
return links;
}, [toolCalls]);
const isCredible = useMemo(() => {
return checkLinkCredibility && href && !responding
? credibleLinks.has(href)
: true;
}, [credibleLinks, href, responding, checkLinkCredibility]);
return (
<span className="flex items-center gap-1.5">
<a href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
{!isCredible && (
<Tooltip
title="This link might be a hallucination from AI model and may not be reliable."
delayDuration={300}
>
<WarningFilled className="text-sx transition-colors hover:!text-yellow-500" />
</Tooltip>
)}
</span>
);
};
+20 -19
View File
@@ -18,6 +18,7 @@ import { cn } from "~/lib/utils";
import Image from "./image";
import { Tooltip } from "./tooltip";
import { Link } from "./link";
export function Markdown({
className,
@@ -25,13 +26,30 @@ export function Markdown({
style,
enableCopy,
animated = false,
checkLinkCredibility = false,
...props
}: ReactMarkdownOptions & {
className?: string;
enableCopy?: boolean;
style?: React.CSSProperties;
animated?: boolean;
checkLinkCredibility?: boolean;
}) {
const components: ReactMarkdownOptions["components"] = useMemo(() => {
return {
a: ({ href, children }) => (
<Link href={href} checkLinkCredibility={checkLinkCredibility}>
{children}
</Link>
),
img: ({ src, alt }) => (
<a href={src as string} target="_blank" rel="noopener noreferrer">
<Image className="rounded" src={src as string} alt={alt ?? ""} />
</a>
),
};
}, [checkLinkCredibility]);
const rehypePlugins = useMemo(() => {
if (animated) {
return [rehypeKatex, rehypeSplitWordsIntoSpans];
@@ -39,28 +57,11 @@ export function Markdown({
return [rehypeKatex];
}, [animated]);
return (
<div
className={cn(
className,
"prose dark:prose-invert prose-p:my-0 prose-img:mt-0 flex flex-col gap-4",
)}
style={style}
>
<div className={cn(className, "prose dark:prose-invert")} style={style}>
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={rehypePlugins}
components={{
a: ({ href, children }) => (
<a href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
),
img: ({ src, alt }) => (
<a href={src as string} target="_blank" rel="noopener noreferrer">
<Image className="rounded" src={src as string} alt={alt ?? ""} />
</a>
),
}}
components={components}
{...props}
>
{autoFixMarkdown(
+3 -1
View File
@@ -19,6 +19,7 @@ export function Tooltip({
open,
side,
sideOffset,
delayDuration = 750,
}: {
className?: string;
style?: CSSProperties;
@@ -27,10 +28,11 @@ export function Tooltip({
open?: boolean;
side?: "left" | "right" | "top" | "bottom";
sideOffset?: number;
delayDuration?: number;
}) {
return (
<TooltipProvider>
<ShadcnTooltip delayDuration={750} open={open}>
<ShadcnTooltip delayDuration={delayDuration} open={open}>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent
className={cn(className)}
+3 -10
View File
@@ -78,16 +78,12 @@ const taskItem = TaskItem.configure({
});
const horizontalRule = HorizontalRule.configure({
HTMLAttributes: {
class: cx("mt-4 mb-6 border-t border-muted-foreground"),
},
HTMLAttributes: {},
});
const starterKit = StarterKit.configure({
bulletList: {
HTMLAttributes: {
class: cx("list-disc list-outside leading-3 -mt-2"),
},
HTMLAttributes: {},
},
orderedList: {
HTMLAttributes: {
@@ -95,9 +91,7 @@ const starterKit = StarterKit.configure({
},
},
listItem: {
HTMLAttributes: {
class: cx("leading-normal -mb-2"),
},
HTMLAttributes: {},
},
blockquote: {
HTMLAttributes: {
@@ -107,7 +101,6 @@ const starterKit = StarterKit.configure({
codeBlock: false,
code: {
HTMLAttributes: {
class: cx("rounded-md bg-muted px-1.5 py-1 font-mono font-medium"),
spellcheck: "false",
},
},
+11
View File
@@ -377,3 +377,14 @@ export function useLastFeedbackMessageId() {
);
return waitingForFeedbackMessageId;
}
export function useToolCalls() {
return useStore(
useShallow((state) => {
return state.messageIds
?.map((id) => getMessage(id)?.toolCalls)
.filter((toolCalls) => toolCalls != null)
.flat();
}),
);
}
+4
View File
@@ -4,6 +4,10 @@
color: inherit;
}
.ProseMirror {
line-height: 1.75;
}
.ProseMirror .is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;