Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0c6f450d4 | |||
| a43db94fb6 | |||
| 8802eea0ba | |||
| 1a59accb52 | |||
| 86295ed195 | |||
| bf4820c68f | |||
| 25e7b86f02 | |||
| 0459e3c9f8 | |||
| 5cc0e61297 | |||
| a220f4b6ea | |||
| f73a7a229c | |||
| d983149984 | |||
| 3d5e579ebd | |||
| a14ca92c36 | |||
| b85a7592dc |
@@ -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`
|
||||
|
||||
@@ -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
|
||||
@@ -21,3 +21,6 @@ conf.yaml
|
||||
.idea/
|
||||
.langgraph_api/
|
||||
|
||||
# coverage report
|
||||
coverage.xml
|
||||
coverage/
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://deepwiki.com/bytedance/deer-flow)
|
||||
[](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:
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 でも実行できます。
|
||||
|
||||
@@ -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 运行此项目。
|
||||
|
||||
@@ -52,6 +52,9 @@ filterwarnings = [
|
||||
"ignore::UserWarning",
|
||||
]
|
||||
|
||||
[tool.coverage.report]
|
||||
fail_under = 25
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src"]
|
||||
|
||||
|
||||
+22
-5
@@ -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
@@ -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
@@ -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")
|
||||
@@ -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();
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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(
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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,6 +4,10 @@
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.ProseMirror .is-editor-empty:first-child::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
|
||||
Reference in New Issue
Block a user