mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-20 07:01:03 +00:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c9fdfef6b | |||
| 933f3bb83a | |||
| f17b06f206 | |||
| c14c548e0c | |||
| c89b35805d | |||
| 774473cc18 | |||
| b04225b7c8 | |||
| b155e1eca6 | |||
| 448001f532 | |||
| 0f118fda92 | |||
| ae30517f48 | |||
| 8bdc6bfa2d | |||
| afbcdd68d8 | |||
| bf3bcee8e3 | |||
| 0c46f8361b | |||
| 86a89acac3 | |||
| 2121510f63 | |||
| 0dc6c16c42 | |||
| 5abf8c1f5e | |||
| 70b86d8464 | |||
| e1187d7d02 | |||
| 136f7eaa4e | |||
| 3c46201ff0 | |||
| 2363b21447 | |||
| 0d3255cdae | |||
| 9f8f060506 | |||
| dfd4712d9f | |||
| 859c6e3c5d | |||
| d8016809b2 | |||
| 6c254c0783 | |||
| d4fbc86b28 | |||
| 7ad11bf86c | |||
| be893eae2b | |||
| 5977b4a03e | |||
| 52dfdd83ae | |||
| f27c96e692 | |||
| b7373fbe70 | |||
| 9c2d4724e3 | |||
| aa06cd6fb6 | |||
| 82e1b65792 | |||
| dcdd7288ed | |||
| 89f3d731c9 | |||
| c0b04aaba2 | |||
| 4048ca67dd | |||
| 30a189cf26 | |||
| e03b12b97f | |||
| 8823ffdb6a | |||
| 4fe43153b1 | |||
| 4fb053b6d2 | |||
| 19fa1e97c3 | |||
| a7315b46df | |||
| 03e6a1a6e7 | |||
| 7d38e5f900 | |||
| 4c2fe2e7f5 | |||
| bb7dc6e98c | |||
| ee1af78767 | |||
| 2554e4ba63 |
@@ -7,6 +7,11 @@ NEXT_PUBLIC_API_URL="http://localhost:8000/api"
|
|||||||
|
|
||||||
AGENT_RECURSION_LIMIT=30
|
AGENT_RECURSION_LIMIT=30
|
||||||
|
|
||||||
|
# CORS settings
|
||||||
|
# Comma-separated list of allowed origins for CORS requests
|
||||||
|
# Example: ALLOWED_ORIGINS=http://localhost:3000,http://example.com
|
||||||
|
ALLOWED_ORIGINS=http://localhost:3000
|
||||||
|
|
||||||
# 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
|
||||||
@@ -14,6 +19,12 @@ TAVILY_API_KEY=tvly-xxx
|
|||||||
# JINA_API_KEY=jina_xxx # Optional, default is None
|
# JINA_API_KEY=jina_xxx # Optional, default is None
|
||||||
|
|
||||||
# Optional, RAG provider
|
# Optional, RAG provider
|
||||||
|
# RAG_PROVIDER=vikingdb_knowledge_base
|
||||||
|
# VIKINGDB_KNOWLEDGE_BASE_API_URL="api-knowledgebase.mlp.cn-beijing.volces.com"
|
||||||
|
# VIKINGDB_KNOWLEDGE_BASE_API_AK="AKxxx"
|
||||||
|
# VIKINGDB_KNOWLEDGE_BASE_API_SK=""
|
||||||
|
# VIKINGDB_KNOWLEDGE_BASE_RETRIEVAL_SIZE=15
|
||||||
|
|
||||||
# RAG_PROVIDER=ragflow
|
# RAG_PROVIDER=ragflow
|
||||||
# RAGFLOW_API_URL="http://localhost:9388"
|
# RAGFLOW_API_URL="http://localhost:9388"
|
||||||
# RAGFLOW_API_KEY="ragflow-xxx"
|
# RAGFLOW_API_KEY="ragflow-xxx"
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
name: Publish Containers
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
backend-container:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
attestations: write
|
||||||
|
id-token: write
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
- name: Build and push Docker image
|
||||||
|
id: push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
|
- name: Generate artifact attestation
|
||||||
|
uses: actions/attest-build-provenance@v2
|
||||||
|
with:
|
||||||
|
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||||
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
push-to-registry: true
|
||||||
|
|
||||||
|
frontend-container:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
attestations: write
|
||||||
|
id-token: write
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}-web
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 #v3.4.0
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 #v5.7.0
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
- name: Build and push Docker image
|
||||||
|
id: push
|
||||||
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
|
||||||
|
with:
|
||||||
|
context: web
|
||||||
|
file: web/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
|
- name: Generate artifact attestation
|
||||||
|
uses: actions/attest-build-provenance@v2
|
||||||
|
with:
|
||||||
|
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||||
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
push-to-registry: true
|
||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: astral-sh/setup-uv@v5
|
uses: astral-sh/setup-uv@v6.3.1
|
||||||
with:
|
with:
|
||||||
version: "latest"
|
version: "latest"
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: astral-sh/setup-uv@v5
|
uses: astral-sh/setup-uv@v6.3.1
|
||||||
with:
|
with:
|
||||||
version: "latest"
|
version: "latest"
|
||||||
|
|
||||||
|
|||||||
Vendored
+30
@@ -1,6 +1,36 @@
|
|||||||
{
|
{
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Debug Tests",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "launch",
|
||||||
|
"module": "pytest",
|
||||||
|
"args": [
|
||||||
|
"${workspaceFolder}/tests",
|
||||||
|
"-v",
|
||||||
|
"-s"
|
||||||
|
],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"justMyCode": false,
|
||||||
|
"env": {
|
||||||
|
"PYTHONPATH": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug Current Test File",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "launch",
|
||||||
|
"module": "pytest",
|
||||||
|
"args": [
|
||||||
|
"${file}",
|
||||||
|
"-v",
|
||||||
|
"-s"
|
||||||
|
],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"justMyCode": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Python: 当前文件",
|
"name": "Python: 当前文件",
|
||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
|
|||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"python.testing.pytestArgs": [
|
||||||
|
"tests"
|
||||||
|
],
|
||||||
|
"python.testing.unittestEnabled": false,
|
||||||
|
"python.testing.pytestEnabled": true
|
||||||
|
}
|
||||||
@@ -17,11 +17,14 @@ There are many ways you can contribute to DeerFlow:
|
|||||||
|
|
||||||
1. Fork the repository
|
1. Fork the repository
|
||||||
2. Clone your fork:
|
2. Clone your fork:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/bytedance/deer-flow.git
|
git clone https://github.com/bytedance/deer-flow.git
|
||||||
cd deer-flow
|
cd deer-flow
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Set up your development environment:
|
3. Set up your development environment:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies, uv will take care of the python interpreter and venv creation
|
# Install dependencies, uv will take care of the python interpreter and venv creation
|
||||||
uv sync
|
uv sync
|
||||||
@@ -30,7 +33,9 @@ There are many ways you can contribute to DeerFlow:
|
|||||||
uv pip install -e ".[dev]"
|
uv pip install -e ".[dev]"
|
||||||
uv pip install -e ".[test]"
|
uv pip install -e ".[test]"
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Configure pre-commit hooks:
|
4. Configure pre-commit hooks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
chmod +x pre-commit
|
chmod +x pre-commit
|
||||||
ln -s ../../pre-commit .git/hooks/pre-commit
|
ln -s ../../pre-commit .git/hooks/pre-commit
|
||||||
@@ -39,6 +44,7 @@ There are many ways you can contribute to DeerFlow:
|
|||||||
## Development Process
|
## Development Process
|
||||||
|
|
||||||
1. Create a new branch:
|
1. Create a new branch:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git checkout -b feature/amazing-feature
|
git checkout -b feature/amazing-feature
|
||||||
```
|
```
|
||||||
@@ -50,6 +56,7 @@ There are many ways you can contribute to DeerFlow:
|
|||||||
- Update documentation as needed
|
- Update documentation as needed
|
||||||
|
|
||||||
3. Run tests and checks:
|
3. Run tests and checks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make test # Run tests
|
make test # Run tests
|
||||||
make lint # Run linting
|
make lint # Run linting
|
||||||
@@ -58,11 +65,13 @@ There are many ways you can contribute to DeerFlow:
|
|||||||
```
|
```
|
||||||
|
|
||||||
4. Commit your changes:
|
4. Commit your changes:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git commit -m 'Add some amazing feature'
|
git commit -m 'Add some amazing feature'
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Push to your fork:
|
5. Push to your fork:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git push origin feature/amazing-feature
|
git push origin feature/amazing-feature
|
||||||
```
|
```
|
||||||
@@ -90,6 +99,7 @@ There are many ways you can contribute to DeerFlow:
|
|||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Run the test suite:
|
Run the test suite:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests
|
# Run all tests
|
||||||
make test
|
make test
|
||||||
@@ -122,6 +132,7 @@ make format
|
|||||||
## Need Help?
|
## Need Help?
|
||||||
|
|
||||||
If you need help with anything:
|
If you need help with anything:
|
||||||
|
|
||||||
- Check existing issues and discussions
|
- Check existing issues and discussions
|
||||||
- Join our community channels
|
- Join our community channels
|
||||||
- Ask questions in discussions
|
- Ask questions in discussions
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
|
FROM ghcr.io/astral-sh/uv:python3.12-bookworm
|
||||||
|
|
||||||
# Install uv.
|
# Install uv.
|
||||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ format:
|
|||||||
|
|
||||||
lint:
|
lint:
|
||||||
uv run black --check .
|
uv run black --check .
|
||||||
|
uv run ruff check .
|
||||||
|
|
||||||
serve:
|
serve:
|
||||||
uv run server.py --reload
|
uv run server.py --reload
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
|
|
||||||
**DeerFlow** (**D**eep **E**xploration and **E**fficient **R**esearch **Flow**) is a community-driven Deep Research framework that builds upon the incredible work of the open source community. Our goal is to combine language models with specialized tools for tasks like web search, crawling, and Python code execution, while giving back to the community that made this possible.
|
**DeerFlow** (**D**eep **E**xploration and **E**fficient **R**esearch **Flow**) is a community-driven Deep Research framework that builds upon the incredible work of the open source community. Our goal is to combine language models with specialized tools for tasks like web search, crawling, and Python code execution, while giving back to the community that made this possible.
|
||||||
|
|
||||||
Currently, DeerFlow has officially entered the FaaS Application Center of Volcengine. Users can experience it online through the experience link to intuitively feel its powerful functions and convenient operations. At the same time, to meet the deployment needs of different users, DeerFlow supports one-click deployment based on Volcengine. Click the deployment link to quickly complete the deployment process and start an efficient research journey.
|
Currently, DeerFlow has officially entered the [FaaS Application Center of Volcengine](https://console.volcengine.com/vefaas/region:vefaas+cn-beijing/market). Users can experience it online through the [experience link](https://console.volcengine.com/vefaas/region:vefaas+cn-beijing/market/deerflow/?channel=github&source=deerflow) to intuitively feel its powerful functions and convenient operations. At the same time, to meet the deployment needs of different users, DeerFlow supports one-click deployment based on Volcengine. Click the [deployment link](https://console.volcengine.com/vefaas/region:vefaas+cn-beijing/application/create?templateId=683adf9e372daa0008aaed5c&channel=github&source=deerflow) to quickly complete the deployment process and start an efficient research journey.
|
||||||
|
|
||||||
|
|
||||||
Please visit [our official website](https://deerflow.tech/) for more details.
|
Please visit [our official website](https://deerflow.tech/) for more details.
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ Please visit [our official website](https://deerflow.tech/) for more details.
|
|||||||
|
|
||||||
### Video
|
### Video
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/f3786598-1f2a-4d07-919e-8b99dfa1de3e
|
<https://github.com/user-attachments/assets/f3786598-1f2a-4d07-919e-8b99dfa1de3e>
|
||||||
|
|
||||||
In this demo, we showcase how to use DeerFlow to:
|
In this demo, we showcase how to use DeerFlow to:
|
||||||
|
|
||||||
@@ -146,19 +147,18 @@ Explore more details in the [`web`](./web/) directory.
|
|||||||
|
|
||||||
## Supported Search Engines
|
## Supported Search Engines
|
||||||
|
|
||||||
|
### Web Search
|
||||||
|
|
||||||
DeerFlow supports multiple search engines that can be configured in your `.env` file using the `SEARCH_API` variable:
|
DeerFlow supports multiple search engines that can be configured in your `.env` file using the `SEARCH_API` variable:
|
||||||
|
|
||||||
- **Tavily** (default): A specialized search API for AI applications
|
- **Tavily** (default): A specialized search API for AI applications
|
||||||
|
|
||||||
- Requires `TAVILY_API_KEY` in your `.env` file
|
- Requires `TAVILY_API_KEY` in your `.env` file
|
||||||
- Sign up at: https://app.tavily.com/home
|
- Sign up at: https://app.tavily.com/home
|
||||||
|
|
||||||
- **DuckDuckGo**: Privacy-focused search engine
|
- **DuckDuckGo**: Privacy-focused search engine
|
||||||
|
|
||||||
- No API key required
|
- No API key required
|
||||||
|
|
||||||
- **Brave Search**: Privacy-focused search engine with advanced features
|
- **Brave Search**: Privacy-focused search engine with advanced features
|
||||||
|
|
||||||
- Requires `BRAVE_SEARCH_API_KEY` in your `.env` file
|
- Requires `BRAVE_SEARCH_API_KEY` in your `.env` file
|
||||||
- Sign up at: https://brave.com/search/api/
|
- Sign up at: https://brave.com/search/api/
|
||||||
|
|
||||||
@@ -173,6 +173,19 @@ To configure your preferred search engine, set the `SEARCH_API` variable in your
|
|||||||
SEARCH_API=tavily
|
SEARCH_API=tavily
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Private Knowledgebase
|
||||||
|
|
||||||
|
DeerFlow support private knowledgebase such as ragflow and vikingdb, so that you can use your private documents to answer questions.
|
||||||
|
|
||||||
|
- **[RAGFlow](https://ragflow.io/docs/dev/)**:open source RAG engine
|
||||||
|
```
|
||||||
|
# examples in .env.example
|
||||||
|
RAG_PROVIDER=ragflow
|
||||||
|
RAGFLOW_API_URL="http://localhost:9388"
|
||||||
|
RAGFLOW_API_KEY="ragflow-xxx"
|
||||||
|
RAGFLOW_RETRIEVAL_SIZE=10
|
||||||
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Core Capabilities
|
### Core Capabilities
|
||||||
@@ -186,23 +199,15 @@ SEARCH_API=tavily
|
|||||||
### Tools and MCP Integrations
|
### Tools and MCP Integrations
|
||||||
|
|
||||||
- 🔍 **Search and Retrieval**
|
- 🔍 **Search and Retrieval**
|
||||||
|
|
||||||
- Web search via Tavily, Brave Search and more
|
- Web search via Tavily, Brave Search and more
|
||||||
- Crawling with Jina
|
- Crawling with Jina
|
||||||
- Advanced content extraction
|
- Advanced content extraction
|
||||||
|
- Support for private knowledgebase
|
||||||
|
|
||||||
- 📃 **RAG Integration**
|
- 📃 **RAG Integration**
|
||||||
|
|
||||||
- Supports mentioning files from [RAGFlow](https://github.com/infiniflow/ragflow) within the input box. [Start up RAGFlow server](https://ragflow.io/docs/dev/).
|
- Supports mentioning files from [RAGFlow](https://github.com/infiniflow/ragflow) within the input box. [Start up RAGFlow server](https://ragflow.io/docs/dev/).
|
||||||
|
|
||||||
```bash
|
|
||||||
# .env
|
|
||||||
RAG_PROVIDER=ragflow
|
|
||||||
RAGFLOW_API_URL="http://localhost:9388"
|
|
||||||
RAGFLOW_API_KEY="ragflow-xxx"
|
|
||||||
RAGFLOW_RETRIEVAL_SIZE=10
|
|
||||||
```
|
|
||||||
|
|
||||||
- 🔗 **MCP Seamless Integration**
|
- 🔗 **MCP Seamless Integration**
|
||||||
- Expand capabilities for private domain access, knowledge graph, web browsing and more
|
- Expand capabilities for private domain access, knowledge graph, web browsing and more
|
||||||
- Facilitates integration of diverse research tools and methodologies
|
- Facilitates integration of diverse research tools and methodologies
|
||||||
@@ -210,7 +215,6 @@ SEARCH_API=tavily
|
|||||||
### Human Collaboration
|
### Human Collaboration
|
||||||
|
|
||||||
- 🧠 **Human-in-the-loop**
|
- 🧠 **Human-in-the-loop**
|
||||||
|
|
||||||
- Supports interactive modification of research plans using natural language
|
- Supports interactive modification of research plans using natural language
|
||||||
- Supports auto-acceptance of research plans
|
- Supports auto-acceptance of research plans
|
||||||
|
|
||||||
@@ -518,6 +522,7 @@ DeerFlow includes a human in the loop mechanism that allows you to review, edit,
|
|||||||
- Via API: Set `auto_accepted_plan: true` in your request
|
- Via API: Set `auto_accepted_plan: true` in your request
|
||||||
|
|
||||||
4. **API Integration**: When using the API, you can provide feedback through the `feedback` parameter:
|
4. **API Integration**: When using the API, you can provide feedback through the `feedback` parameter:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"messages": [{ "role": "user", "content": "What is quantum computing?" }],
|
"messages": [{ "role": "user", "content": "What is quantum computing?" }],
|
||||||
|
|||||||
+40
-33
@@ -19,9 +19,10 @@ Besuchen Sie [unsere offizielle Website](https://deerflow.tech/) für weitere De
|
|||||||
|
|
||||||
### Video
|
### Video
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/f3786598-1f2a-4d07-919e-8b99dfa1de3e
|
<https://github.com/user-attachments/assets/f3786598-1f2a-4d07-919e-8b99dfa1de3e>
|
||||||
|
|
||||||
In dieser Demo zeigen wir, wie man DeerFlow nutzt, um:
|
In dieser Demo zeigen wir, wie man DeerFlow nutzt, um:
|
||||||
|
|
||||||
- Nahtlos mit MCP-Diensten zu integrieren
|
- Nahtlos mit MCP-Diensten zu integrieren
|
||||||
- Den Prozess der tiefgehenden Recherche durchzuführen und einen umfassenden Bericht mit Bildern zu erstellen
|
- Den Prozess der tiefgehenden Recherche durchzuführen und einen umfassenden Bericht mit Bildern zu erstellen
|
||||||
- Podcast-Audio basierend auf dem generierten Bericht zu erstellen
|
- Podcast-Audio basierend auf dem generierten Bericht zu erstellen
|
||||||
@@ -36,7 +37,6 @@ In dieser Demo zeigen wir, wie man DeerFlow nutzt, um:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
## 📑 Inhaltsverzeichnis
|
## 📑 Inhaltsverzeichnis
|
||||||
|
|
||||||
- [🚀 Schnellstart](#schnellstart)
|
- [🚀 Schnellstart](#schnellstart)
|
||||||
@@ -50,12 +50,12 @@ In dieser Demo zeigen wir, wie man DeerFlow nutzt, um:
|
|||||||
- [💖 Danksagungen](#danksagungen)
|
- [💖 Danksagungen](#danksagungen)
|
||||||
- [⭐ Star-Verlauf](#star-verlauf)
|
- [⭐ Star-Verlauf](#star-verlauf)
|
||||||
|
|
||||||
|
|
||||||
## Schnellstart
|
## Schnellstart
|
||||||
|
|
||||||
DeerFlow ist in Python entwickelt und kommt mit einer in Node.js geschriebenen Web-UI. Um einen reibungslosen Einrichtungsprozess zu gewährleisten, empfehlen wir die Verwendung der folgenden Tools:
|
DeerFlow ist in Python entwickelt und kommt mit einer in Node.js geschriebenen Web-UI. Um einen reibungslosen Einrichtungsprozess zu gewährleisten, empfehlen wir die Verwendung der folgenden Tools:
|
||||||
|
|
||||||
### Empfohlene Tools
|
### Empfohlene Tools
|
||||||
|
|
||||||
- **[`uv`](https://docs.astral.sh/uv/getting-started/installation/):**
|
- **[`uv`](https://docs.astral.sh/uv/getting-started/installation/):**
|
||||||
Vereinfacht die Verwaltung von Python-Umgebungen und Abhängigkeiten. `uv` erstellt automatisch eine virtuelle Umgebung im Stammverzeichnis und installiert alle erforderlichen Pakete für Sie—keine manuelle Installation von Python-Umgebungen notwendig.
|
Vereinfacht die Verwaltung von Python-Umgebungen und Abhängigkeiten. `uv` erstellt automatisch eine virtuelle Umgebung im Stammverzeichnis und installiert alle erforderlichen Pakete für Sie—keine manuelle Installation von Python-Umgebungen notwendig.
|
||||||
|
|
||||||
@@ -66,11 +66,14 @@ DeerFlow ist in Python entwickelt und kommt mit einer in Node.js geschriebenen W
|
|||||||
Installieren und verwalten Sie Abhängigkeiten des Node.js-Projekts.
|
Installieren und verwalten Sie Abhängigkeiten des Node.js-Projekts.
|
||||||
|
|
||||||
### Umgebungsanforderungen
|
### Umgebungsanforderungen
|
||||||
|
|
||||||
Stellen Sie sicher, dass Ihr System die folgenden Mindestanforderungen erfüllt:
|
Stellen Sie sicher, dass Ihr System die folgenden Mindestanforderungen erfüllt:
|
||||||
|
|
||||||
- **[Python](https://www.python.org/downloads/):** Version `3.12+`
|
- **[Python](https://www.python.org/downloads/):** Version `3.12+`
|
||||||
- **[Node.js](https://nodejs.org/en/download/):** Version `22+`
|
- **[Node.js](https://nodejs.org/en/download/):** Version `22+`
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Repository klonen
|
# Repository klonen
|
||||||
git clone https://github.com/bytedance/deer-flow.git
|
git clone https://github.com/bytedance/deer-flow.git
|
||||||
@@ -138,25 +141,24 @@ bootstrap.bat -d
|
|||||||
|
|
||||||
Weitere Details finden Sie im Verzeichnis [`web`](./web/).
|
Weitere Details finden Sie im Verzeichnis [`web`](./web/).
|
||||||
|
|
||||||
|
|
||||||
## Unterstützte Suchmaschinen
|
## Unterstützte Suchmaschinen
|
||||||
|
|
||||||
DeerFlow unterstützt mehrere Suchmaschinen, die in Ihrer `.env`-Datei über die Variable `SEARCH_API` konfiguriert werden können:
|
DeerFlow unterstützt mehrere Suchmaschinen, die in Ihrer `.env`-Datei über die Variable `SEARCH_API` konfiguriert werden können:
|
||||||
|
|
||||||
- **Tavily** (Standard): Eine spezialisierte Such-API für KI-Anwendungen
|
- **Tavily** (Standard): Eine spezialisierte Such-API für KI-Anwendungen
|
||||||
- Erfordert `TAVILY_API_KEY` in Ihrer `.env`-Datei
|
- Erfordert `TAVILY_API_KEY` in Ihrer `.env`-Datei
|
||||||
- Registrieren Sie sich unter: https://app.tavily.com/home
|
- Registrieren Sie sich unter: <https://app.tavily.com/home>
|
||||||
|
|
||||||
- **DuckDuckGo**: Datenschutzorientierte Suchmaschine
|
- **DuckDuckGo**: Datenschutzorientierte Suchmaschine
|
||||||
- Kein API-Schlüssel erforderlich
|
- Kein API-Schlüssel erforderlich
|
||||||
|
|
||||||
- **Brave Search**: Datenschutzorientierte Suchmaschine mit erweiterten Funktionen
|
- **Brave Search**: Datenschutzorientierte Suchmaschine mit erweiterten Funktionen
|
||||||
- Erfordert `BRAVE_SEARCH_API_KEY` in Ihrer `.env`-Datei
|
- Erfordert `BRAVE_SEARCH_API_KEY` in Ihrer `.env`-Datei
|
||||||
- Registrieren Sie sich unter: https://brave.com/search/api/
|
- Registrieren Sie sich unter: <https://brave.com/search/api/>
|
||||||
|
|
||||||
- **Arxiv**: Wissenschaftliche Papiersuche für akademische Forschung
|
- **Arxiv**: Wissenschaftliche Papiersuche für akademische Forschung
|
||||||
- Kein API-Schlüssel erforderlich
|
- Kein API-Schlüssel erforderlich
|
||||||
- Spezialisiert auf wissenschaftliche und akademische Papiere
|
- Spezialisiert auf wissenschaftliche und akademische Papiere
|
||||||
|
|
||||||
Um Ihre bevorzugte Suchmaschine zu konfigurieren, setzen Sie die Variable `SEARCH_API` in Ihrer `.env`-Datei:
|
Um Ihre bevorzugte Suchmaschine zu konfigurieren, setzen Sie die Variable `SEARCH_API` in Ihrer `.env`-Datei:
|
||||||
|
|
||||||
@@ -170,40 +172,39 @@ SEARCH_API=tavily
|
|||||||
### Kernfähigkeiten
|
### Kernfähigkeiten
|
||||||
|
|
||||||
- 🤖 **LLM-Integration**
|
- 🤖 **LLM-Integration**
|
||||||
- Unterstützt die Integration der meisten Modelle über [litellm](https://docs.litellm.ai/docs/providers).
|
- Unterstützt die Integration der meisten Modelle über [litellm](https://docs.litellm.ai/docs/providers).
|
||||||
- Unterstützung für Open-Source-Modelle wie Qwen
|
- Unterstützung für Open-Source-Modelle wie Qwen
|
||||||
- OpenAI-kompatible API-Schnittstelle
|
- OpenAI-kompatible API-Schnittstelle
|
||||||
- Mehrstufiges LLM-System für unterschiedliche Aufgabenkomplexitäten
|
- Mehrstufiges LLM-System für unterschiedliche Aufgabenkomplexitäten
|
||||||
|
|
||||||
### Tools und MCP-Integrationen
|
### Tools und MCP-Integrationen
|
||||||
|
|
||||||
- 🔍 **Suche und Abruf**
|
- 🔍 **Suche und Abruf**
|
||||||
- Websuche über Tavily, Brave Search und mehr
|
- Websuche über Tavily, Brave Search und mehr
|
||||||
- Crawling mit Jina
|
- Crawling mit Jina
|
||||||
- Fortgeschrittene Inhaltsextraktion
|
- Fortgeschrittene Inhaltsextraktion
|
||||||
|
|
||||||
- 🔗 **MCP Nahtlose Integration**
|
- 🔗 **MCP Nahtlose Integration**
|
||||||
- Erweiterte Fähigkeiten für privaten Domänenzugriff, Wissensgraphen, Webbrowsing und mehr
|
- Erweiterte Fähigkeiten für privaten Domänenzugriff, Wissensgraphen, Webbrowsing und mehr
|
||||||
- Erleichtert die Integration verschiedener Forschungswerkzeuge und -methoden
|
- Erleichtert die Integration verschiedener Forschungswerkzeuge und -methoden
|
||||||
|
|
||||||
### Menschliche Zusammenarbeit
|
### Menschliche Zusammenarbeit
|
||||||
|
|
||||||
- 🧠 **Mensch-in-der-Schleife**
|
- 🧠 **Mensch-in-der-Schleife**
|
||||||
- Unterstützt interaktive Modifikation von Forschungsplänen mit natürlicher Sprache
|
- Unterstützt interaktive Modifikation von Forschungsplänen mit natürlicher Sprache
|
||||||
- Unterstützt automatische Akzeptanz von Forschungsplänen
|
- Unterstützt automatische Akzeptanz von Forschungsplänen
|
||||||
|
|
||||||
- 📝 **Bericht-Nachbearbeitung**
|
- 📝 **Bericht-Nachbearbeitung**
|
||||||
- Unterstützt Notion-ähnliche Blockbearbeitung
|
- Unterstützt Notion-ähnliche Blockbearbeitung
|
||||||
- Ermöglicht KI-Verfeinerungen, einschließlich KI-unterstützter Polierung, Satzkürzung und -erweiterung
|
- Ermöglicht KI-Verfeinerungen, einschließlich KI-unterstützter Polierung, Satzkürzung und -erweiterung
|
||||||
- Angetrieben von [tiptap](https://tiptap.dev/)
|
- Angetrieben von [tiptap](https://tiptap.dev/)
|
||||||
|
|
||||||
### Inhaltserstellung
|
### Inhaltserstellung
|
||||||
|
|
||||||
- 🎙️ **Podcast- und Präsentationserstellung**
|
- 🎙️ **Podcast- und Präsentationserstellung**
|
||||||
- KI-gestützte Podcast-Skripterstellung und Audiosynthese
|
- KI-gestützte Podcast-Skripterstellung und Audiosynthese
|
||||||
- Automatisierte Erstellung einfacher PowerPoint-Präsentationen
|
- Automatisierte Erstellung einfacher PowerPoint-Präsentationen
|
||||||
- Anpassbare Vorlagen für maßgeschneiderte Inhalte
|
- Anpassbare Vorlagen für maßgeschneiderte Inhalte
|
||||||
|
|
||||||
|
|
||||||
## Architektur
|
## Architektur
|
||||||
|
|
||||||
@@ -255,7 +256,6 @@ curl --location 'http://localhost:8000/api/tts' \
|
|||||||
--output speech.mp3
|
--output speech.mp3
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Entwicklung
|
## Entwicklung
|
||||||
|
|
||||||
### Testen
|
### Testen
|
||||||
@@ -313,9 +313,10 @@ langgraph dev
|
|||||||
```
|
```
|
||||||
|
|
||||||
Nach dem Start des LangGraph-Servers sehen Sie mehrere URLs im Terminal:
|
Nach dem Start des LangGraph-Servers sehen Sie mehrere URLs im Terminal:
|
||||||
- API: http://127.0.0.1:2024
|
|
||||||
- Studio UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
|
- API: <http://127.0.0.1:2024>
|
||||||
- API-Dokumentation: http://127.0.0.1:2024/docs
|
- Studio UI: <https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024>
|
||||||
|
- API-Dokumentation: <http://127.0.0.1:2024/docs>
|
||||||
|
|
||||||
Öffnen Sie den Studio UI-Link in Ihrem Browser, um auf die Debugging-Schnittstelle zuzugreifen.
|
Öffnen Sie den Studio UI-Link in Ihrem Browser, um auf die Debugging-Schnittstelle zuzugreifen.
|
||||||
|
|
||||||
@@ -330,6 +331,7 @@ In der Studio UI können Sie:
|
|||||||
5. Feedback während der Planungsphase geben, um Forschungspläne zu verfeinern
|
5. Feedback während der Planungsphase geben, um Forschungspläne zu verfeinern
|
||||||
|
|
||||||
Wenn Sie ein Forschungsthema in der Studio UI einreichen, können Sie die gesamte Workflow-Ausführung sehen, einschließlich:
|
Wenn Sie ein Forschungsthema in der Studio UI einreichen, können Sie die gesamte Workflow-Ausführung sehen, einschließlich:
|
||||||
|
|
||||||
- Die Planungsphase, in der der Forschungsplan erstellt wird
|
- Die Planungsphase, in der der Forschungsplan erstellt wird
|
||||||
- Die Feedback-Schleife, in der Sie den Plan ändern können
|
- Die Feedback-Schleife, in der Sie den Plan ändern können
|
||||||
- Die Forschungs- und Schreibphasen für jeden Abschnitt
|
- Die Forschungs- und Schreibphasen für jeden Abschnitt
|
||||||
@@ -340,6 +342,7 @@ Wenn Sie ein Forschungsthema in der Studio UI einreichen, können Sie die gesamt
|
|||||||
DeerFlow unterstützt LangSmith-Tracing, um Ihnen beim Debuggen und Überwachen Ihrer Workflows zu helfen. Um LangSmith-Tracing zu aktivieren:
|
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`):
|
1. Stellen Sie sicher, dass Ihre `.env`-Datei die folgenden Konfigurationen enthält (siehe `.env.example`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
LANGSMITH_TRACING=true
|
LANGSMITH_TRACING=true
|
||||||
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
|
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
|
||||||
@@ -348,6 +351,7 @@ DeerFlow unterstützt LangSmith-Tracing, um Ihnen beim Debuggen und Überwachen
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. Starten Sie das Tracing mit LangSmith lokal, indem Sie folgenden Befehl ausführen:
|
2. Starten Sie das Tracing mit LangSmith lokal, indem Sie folgenden Befehl ausführen:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
langgraph dev
|
langgraph dev
|
||||||
```
|
```
|
||||||
@@ -421,6 +425,7 @@ uv run main.py --help
|
|||||||
Die Anwendung unterstützt jetzt einen interaktiven Modus mit eingebauten Fragen in Englisch und Chinesisch:
|
Die Anwendung unterstützt jetzt einen interaktiven Modus mit eingebauten Fragen in Englisch und Chinesisch:
|
||||||
|
|
||||||
1. Starten Sie den interaktiven Modus:
|
1. Starten Sie den interaktiven Modus:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run main.py --interactive
|
uv run main.py --interactive
|
||||||
```
|
```
|
||||||
@@ -446,6 +451,7 @@ DeerFlow enthält einen Mensch-in-der-Schleife-Mechanismus, der es Ihnen ermögl
|
|||||||
- Über API: Setzen Sie `auto_accepted_plan: true` in Ihrer Anfrage
|
- Über API: Setzen Sie `auto_accepted_plan: true` in Ihrer Anfrage
|
||||||
|
|
||||||
4. **API-Integration**: Bei Verwendung der API können Sie Feedback über den Parameter `feedback` geben:
|
4. **API-Integration**: Bei Verwendung der API können Sie Feedback über den Parameter `feedback` geben:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"messages": [{"role": "user", "content": "Was ist Quantencomputing?"}],
|
"messages": [{"role": "user", "content": "Was ist Quantencomputing?"}],
|
||||||
@@ -485,6 +491,7 @@ Wir möchten unsere aufrichtige Wertschätzung den folgenden Projekten für ihre
|
|||||||
Diese Projekte veranschaulichen die transformative Kraft der Open-Source-Zusammenarbeit, und wir sind stolz darauf, auf ihren Grundlagen aufzubauen.
|
Diese Projekte veranschaulichen die transformative Kraft der Open-Source-Zusammenarbeit, und wir sind stolz darauf, auf ihren Grundlagen aufzubauen.
|
||||||
|
|
||||||
### Hauptmitwirkende
|
### Hauptmitwirkende
|
||||||
|
|
||||||
Ein herzliches Dankeschön geht an die Hauptautoren von `DeerFlow`, deren Vision, Leidenschaft und Engagement dieses Projekt zum Leben erweckt haben:
|
Ein herzliches Dankeschön geht an die Hauptautoren von `DeerFlow`, deren Vision, Leidenschaft und Engagement dieses Projekt zum Leben erweckt haben:
|
||||||
|
|
||||||
- **[Daniel Walnut](https://github.com/hetaoBackend/)**
|
- **[Daniel Walnut](https://github.com/hetaoBackend/)**
|
||||||
|
|||||||
+9
-6
@@ -19,7 +19,7 @@ Por favor, visita [nuestra página web oficial](https://deerflow.tech/) para má
|
|||||||
|
|
||||||
### Video
|
### Video
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/f3786598-1f2a-4d07-919e-8b99dfa1de3e
|
<https://github.com/user-attachments/assets/f3786598-1f2a-4d07-919e-8b99dfa1de3e>
|
||||||
|
|
||||||
En esta demostración, mostramos cómo usar DeerFlow para:
|
En esta demostración, mostramos cómo usar DeerFlow para:
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ DeerFlow soporta múltiples motores de búsqueda que pueden configurarse en tu a
|
|||||||
- **Tavily** (predeterminado): Una API de búsqueda especializada para aplicaciones de IA
|
- **Tavily** (predeterminado): Una API de búsqueda especializada para aplicaciones de IA
|
||||||
|
|
||||||
- Requiere `TAVILY_API_KEY` en tu archivo `.env`
|
- Requiere `TAVILY_API_KEY` en tu archivo `.env`
|
||||||
- Regístrate en: https://app.tavily.com/home
|
- Regístrate en: <https://app.tavily.com/home>
|
||||||
|
|
||||||
- **DuckDuckGo**: Motor de búsqueda centrado en la privacidad
|
- **DuckDuckGo**: Motor de búsqueda centrado en la privacidad
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ DeerFlow soporta múltiples motores de búsqueda que pueden configurarse en tu a
|
|||||||
- **Brave Search**: Motor de búsqueda centrado en la privacidad con características avanzadas
|
- **Brave Search**: Motor de búsqueda centrado en la privacidad con características avanzadas
|
||||||
|
|
||||||
- Requiere `BRAVE_SEARCH_API_KEY` en tu archivo `.env`
|
- Requiere `BRAVE_SEARCH_API_KEY` en tu archivo `.env`
|
||||||
- Regístrate en: https://brave.com/search/api/
|
- Regístrate en: <https://brave.com/search/api/>
|
||||||
|
|
||||||
- **Arxiv**: Búsqueda de artículos científicos para investigación académica
|
- **Arxiv**: Búsqueda de artículos científicos para investigación académica
|
||||||
- No requiere clave API
|
- No requiere clave API
|
||||||
@@ -325,9 +325,9 @@ langgraph dev
|
|||||||
|
|
||||||
Después de iniciar el servidor LangGraph, verás varias URLs en la terminal:
|
Después de iniciar el servidor LangGraph, verás varias URLs en la terminal:
|
||||||
|
|
||||||
- API: http://127.0.0.1:2024
|
- API: <http://127.0.0.1:2024>
|
||||||
- UI de Studio: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
|
- UI de Studio: <https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024>
|
||||||
- Docs de API: http://127.0.0.1:2024/docs
|
- Docs de API: <http://127.0.0.1:2024/docs>
|
||||||
|
|
||||||
Abre el enlace de UI de Studio en tu navegador para acceder a la interfaz de depuración.
|
Abre el enlace de UI de Studio en tu navegador para acceder a la interfaz de depuración.
|
||||||
|
|
||||||
@@ -353,6 +353,7 @@ Cuando envías un tema de investigación en la UI de Studio, podrás ver toda la
|
|||||||
DeerFlow soporta el rastreo de LangSmith para ayudarte a depurar y monitorear tus flujos de trabajo. Para habilitar el rastreo de LangSmith:
|
DeerFlow soporta el rastreo de LangSmith para ayudarte a depurar y monitorear tus flujos de trabajo. Para habilitar el rastreo de LangSmith:
|
||||||
|
|
||||||
1. Asegúrate de que tu archivo `.env` tenga las siguientes configuraciones (ver `.env.example`):
|
1. Asegúrate de que tu archivo `.env` tenga las siguientes configuraciones (ver `.env.example`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
LANGSMITH_TRACING=true
|
LANGSMITH_TRACING=true
|
||||||
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
|
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
|
||||||
@@ -361,6 +362,7 @@ DeerFlow soporta el rastreo de LangSmith para ayudarte a depurar y monitorear tu
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. Inicia el rastreo y visualiza el grafo localmente con LangSmith ejecutando:
|
2. Inicia el rastreo y visualiza el grafo localmente con LangSmith ejecutando:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
langgraph dev
|
langgraph dev
|
||||||
```
|
```
|
||||||
@@ -504,6 +506,7 @@ DeerFlow incluye un mecanismo de humano en el bucle que te permite revisar, edit
|
|||||||
- Vía API: Establece `auto_accepted_plan: true` en tu solicitud
|
- Vía API: Establece `auto_accepted_plan: true` en tu solicitud
|
||||||
|
|
||||||
4. **Integración API**: Cuando uses la API, puedes proporcionar retroalimentación a través del parámetro `feedback`:
|
4. **Integración API**: Cuando uses la API, puedes proporcionar retroalimentación a través del parámetro `feedback`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"messages": [{ "role": "user", "content": "¿Qué es la computación cuántica?" }],
|
"messages": [{ "role": "user", "content": "¿Qué es la computación cuántica?" }],
|
||||||
|
|||||||
+52
-31
@@ -17,11 +17,11 @@
|
|||||||
|
|
||||||
### ビデオ
|
### ビデオ
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/f3786598-1f2a-4d07-919e-8b99dfa1de3e
|
<https://github.com/user-attachments/assets/f3786598-1f2a-4d07-919e-8b99dfa1de3e>
|
||||||
|
|
||||||
このデモでは、DeerFlow の使用方法を紹介しています:
|
このデモでは、DeerFlowの使用方法を紹介しています:
|
||||||
|
|
||||||
- MCP サービスとのシームレスな統合
|
- MCPサービスとのシームレスな統合
|
||||||
- 深層研究プロセスの実施と画像を含む包括的なレポートの作成
|
- 深層研究プロセスの実施と画像を含む包括的なレポートの作成
|
||||||
- 生成されたレポートに基づくポッドキャストオーディオの作成
|
- 生成されたレポートに基づくポッドキャストオーディオの作成
|
||||||
|
|
||||||
@@ -145,21 +145,18 @@ bootstrap.bat -d
|
|||||||
DeerFlow は複数の検索エンジンをサポートしており、`.env`ファイルの`SEARCH_API`変数で設定できます:
|
DeerFlow は複数の検索エンジンをサポートしており、`.env`ファイルの`SEARCH_API`変数で設定できます:
|
||||||
|
|
||||||
- **Tavily**(デフォルト):AI アプリケーション向けの専門検索 API
|
- **Tavily**(デフォルト):AI アプリケーション向けの専門検索 API
|
||||||
|
|
||||||
- `.env`ファイルに`TAVILY_API_KEY`が必要
|
- `.env`ファイルに`TAVILY_API_KEY`が必要
|
||||||
- 登録先:https://app.tavily.com/home
|
- 登録先:<https://app.tavily.com/home>
|
||||||
|
|
||||||
- **DuckDuckGo**:プライバシー重視の検索エンジン
|
- **DuckDuckGo**:プライバシー重視の検索エンジン
|
||||||
|
- APIキー不要
|
||||||
- API キー不要
|
|
||||||
|
|
||||||
- **Brave Search**:高度な機能を備えたプライバシー重視の検索エンジン
|
- **Brave Search**:高度な機能を備えたプライバシー重視の検索エンジン
|
||||||
|
|
||||||
- `.env`ファイルに`BRAVE_SEARCH_API_KEY`が必要
|
- `.env`ファイルに`BRAVE_SEARCH_API_KEY`が必要
|
||||||
- 登録先:https://brave.com/search/api/
|
- 登録先:<https://brave.com/search/api/>
|
||||||
|
|
||||||
- **Arxiv**:学術研究用の科学論文検索
|
- **Arxiv**:学術研究用の科学論文検索
|
||||||
- API キー不要
|
- APIキー不要
|
||||||
- 科学・学術論文専用
|
- 科学・学術論文専用
|
||||||
|
|
||||||
お好みの検索エンジンを設定するには、`.env`ファイルで`SEARCH_API`変数を設定します:
|
お好みの検索エンジンを設定するには、`.env`ファイルで`SEARCH_API`変数を設定します:
|
||||||
@@ -173,41 +170,39 @@ SEARCH_API=tavily
|
|||||||
|
|
||||||
### コア機能
|
### コア機能
|
||||||
|
|
||||||
- 🤖 **LLM 統合**
|
- 🤖 **LLM統合**
|
||||||
- [litellm](https://docs.litellm.ai/docs/providers)を通じてほとんどのモデルの統合をサポート
|
- [litellm](https://docs.litellm.ai/docs/providers)を通じてほとんどのモデルの統合をサポート
|
||||||
- Qwen などのオープンソースモデルをサポート
|
- Qwenなどのオープンソースモデルをサポート
|
||||||
- OpenAI 互換の API インターフェース
|
- OpenAI互換のAPIインターフェース
|
||||||
- 異なるタスクの複雑さに対応するマルチティア LLM システム
|
- 異なるタスクの複雑さに対応するマルチティアLLMシステム
|
||||||
|
|
||||||
### ツールと MCP 統合
|
### ツールと MCP 統合
|
||||||
|
|
||||||
- 🔍 **検索と取得**
|
- 🔍 **検索と取得**
|
||||||
|
- Tavily、Brave Searchなどを通じたWeb検索
|
||||||
- Tavily、Brave Search などを通じた Web 検索
|
- Jinaを使用したクローリング
|
||||||
- Jina を使用したクローリング
|
|
||||||
- 高度なコンテンツ抽出
|
- 高度なコンテンツ抽出
|
||||||
|
|
||||||
- 🔗 **MCP シームレス統合**
|
- 🔗 **MCPシームレス統合**
|
||||||
- プライベートドメインアクセス、ナレッジグラフ、Web ブラウジングなどの機能を拡張
|
- プライベートドメインアクセス、ナレッジグラフ、Webブラウジングなどの機能を拡張
|
||||||
- 多様な研究ツールと方法論の統合を促進
|
- 多様な研究ツールと方法論の統合を促進
|
||||||
|
|
||||||
### 人間との協力
|
### 人間との協力
|
||||||
|
|
||||||
- 🧠 **人間参加型ループ**
|
- 🧠 **人間参加型ループ**
|
||||||
|
|
||||||
- 自然言語を使用した研究計画の対話的修正をサポート
|
- 自然言語を使用した研究計画の対話的修正をサポート
|
||||||
- 研究計画の自動承認をサポート
|
- 研究計画の自動承認をサポート
|
||||||
|
|
||||||
- 📝 **レポート後編集**
|
- 📝 **レポート後編集**
|
||||||
- Notion ライクなブロック編集をサポート
|
- Notionライクなブロック編集をサポート
|
||||||
- AI 支援による洗練、文の短縮、拡張などの AI 改良を可能に
|
- AI支援による洗練、文の短縮、拡張などのAI改良を可能に
|
||||||
- [tiptap](https://tiptap.dev/)を活用
|
- [tiptap](https://tiptap.dev/)を活用
|
||||||
|
|
||||||
### コンテンツ作成
|
### コンテンツ作成
|
||||||
|
|
||||||
- 🎙️ **ポッドキャストとプレゼンテーション生成**
|
- 🎙️ **ポッドキャストとプレゼンテーション生成**
|
||||||
- AI 駆動のポッドキャストスクリプト生成と音声合成
|
- AI駆動のポッドキャストスクリプト生成と音声合成
|
||||||
- シンプルな PowerPoint プレゼンテーションの自動作成
|
- シンプルなPowerPointプレゼンテーションの自動作成
|
||||||
- カスタマイズ可能なテンプレートで個別のコンテンツに対応
|
- カスタマイズ可能なテンプレートで個別のコンテンツに対応
|
||||||
|
|
||||||
## アーキテクチャ
|
## アーキテクチャ
|
||||||
@@ -243,6 +238,27 @@ DeerFlow は、自動研究とコード分析のためのモジュラーなマ
|
|||||||
- 収集した情報を処理および構造化
|
- 収集した情報を処理および構造化
|
||||||
- 包括的な研究レポートを生成
|
- 包括的な研究レポートを生成
|
||||||
|
|
||||||
|
## テキスト読み上げ統合
|
||||||
|
|
||||||
|
DeerFlowには現在、研究レポートを音声に変換できるテキスト読み上げ(TTS)機能が含まれています。この機能は火山引擎TTS APIを使用して高品質なテキストオーディオを生成します。速度、音量、ピッチなどの特性もカスタマイズ可能です。
|
||||||
|
|
||||||
|
### TTS APIの使用
|
||||||
|
|
||||||
|
`/api/tts`エンドポイントからTTS機能にアクセスできます:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# curlを使用したAPI呼び出し例
|
||||||
|
curl --location 'http://localhost:8000/api/tts' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"text": "これはテキスト読み上げ機能のテストです。",
|
||||||
|
"speed_ratio": 1.0,
|
||||||
|
"volume_ratio": 1.0,
|
||||||
|
"pitch_ratio": 1.0
|
||||||
|
}' \
|
||||||
|
--output speech.mp3
|
||||||
|
```
|
||||||
|
|
||||||
## 開発
|
## 開発
|
||||||
|
|
||||||
### テスト
|
### テスト
|
||||||
@@ -299,11 +315,15 @@ pip install -U "langgraph-cli[inmem]"
|
|||||||
langgraph dev
|
langgraph dev
|
||||||
```
|
```
|
||||||
|
|
||||||
LangGraph サーバーを開始すると、端末にいくつかの URL が表示されます:
|
LangGraphサーバーを開始すると、端末にいくつかのURLが表示されます:
|
||||||
|
|
||||||
- API: http://127.0.0.1:2024
|
- API: <http://127.0.0.1:2024>
|
||||||
- Studio UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
|
- Studio UI: <https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024>
|
||||||
- API ドキュメント: http://127.0.0.1:2024/docs
|
- APIドキュメント: <http://127.0.0.1:2024/docs>
|
||||||
|
|
||||||
|
- API: <http://127.0.0.1:2024>
|
||||||
|
- Studio UI: <https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024>
|
||||||
|
- APIドキュメント: <http://127.0.0.1:2024/docs>
|
||||||
|
|
||||||
ブラウザで Studio UI リンクを開いてデバッグインターフェースにアクセスします。
|
ブラウザで Studio UI リンクを開いてデバッグインターフェースにアクセスします。
|
||||||
|
|
||||||
@@ -317,7 +337,7 @@ Studio UI では、次のことができます:
|
|||||||
4. 各コンポーネントの入力と出力を検査して問題をデバッグ
|
4. 各コンポーネントの入力と出力を検査して問題をデバッグ
|
||||||
5. 計画段階でフィードバックを提供して研究計画を洗練
|
5. 計画段階でフィードバックを提供して研究計画を洗練
|
||||||
|
|
||||||
Studio UI で研究トピックを送信すると、次を含む全ワークフロー実行プロセスを見ることができます:
|
Studio UIで研究トピックを送信すると、次を含む全ワークフロー実行プロセスを見ることができます:
|
||||||
|
|
||||||
- 研究計画を作成する計画段階
|
- 研究計画を作成する計画段階
|
||||||
- 計画を修正できるフィードバックループ
|
- 計画を修正できるフィードバックループ
|
||||||
@@ -329,6 +349,7 @@ Studio UI で研究トピックを送信すると、次を含む全ワークフ
|
|||||||
DeerFlow は LangSmith トレース機能をサポートしており、ワークフローのデバッグとモニタリングに役立ちます。LangSmith トレースを有効にするには:
|
DeerFlow は LangSmith トレース機能をサポートしており、ワークフローのデバッグとモニタリングに役立ちます。LangSmith トレースを有効にするには:
|
||||||
|
|
||||||
1. `.env` ファイルに次の設定があることを確認してください(`.env.example` を参照):
|
1. `.env` ファイルに次の設定があることを確認してください(`.env.example` を参照):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
LANGSMITH_TRACING=true
|
LANGSMITH_TRACING=true
|
||||||
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
|
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
|
||||||
@@ -337,6 +358,7 @@ DeerFlow は LangSmith トレース機能をサポートしており、ワーク
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. 次のコマンドを実行して LangSmith トレースを開始します:
|
2. 次のコマンドを実行して LangSmith トレースを開始します:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
langgraph dev
|
langgraph dev
|
||||||
```
|
```
|
||||||
@@ -498,9 +520,8 @@ DeerFlow には人間参加型ループメカニズムが含まれており、
|
|||||||
|
|
||||||
3. **自動承認**:レビュープロセスをスキップするために自動承認を有効にできます:
|
3. **自動承認**:レビュープロセスをスキップするために自動承認を有効にできます:
|
||||||
|
|
||||||
- API 経由:リクエストで`auto_accepted_plan: true`を設定
|
4. **API統合**:APIを使用する場合、`feedback`パラメータでフィードバックを提供できます:
|
||||||
|
|
||||||
4. **API 統合**:API を使用する場合、`feedback`パラメータでフィードバックを提供できます:
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"messages": [
|
"messages": [
|
||||||
|
|||||||
+8
-10
@@ -20,7 +20,7 @@ Por favor, visite [Nosso Site Oficial](https://deerflow.tech/) para maiores deta
|
|||||||
|
|
||||||
### Video
|
### Video
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/f3786598-1f2a-4d07-919e-8b99dfa1de3e
|
<https://github.com/user-attachments/assets/f3786598-1f2a-4d07-919e-8b99dfa1de3e>
|
||||||
|
|
||||||
Nesse demo, nós demonstramos como usar o DeerFlow para:
|
Nesse demo, nós demonstramos como usar o DeerFlow para:
|
||||||
In this demo, we showcase how to use DeerFlow to:
|
In this demo, we showcase how to use DeerFlow to:
|
||||||
@@ -148,13 +148,12 @@ Explore mais detalhes no diretório [`web`](./web/) .
|
|||||||
|
|
||||||
## Mecanismos de Busca Suportados
|
## Mecanismos de Busca Suportados
|
||||||
|
|
||||||
|
|
||||||
DeerFlow suporta múltiplos mecanismos de busca que podem ser configurados no seu arquivo `.env` usando a variável `SEARCH_API`:
|
DeerFlow suporta múltiplos mecanismos de busca que podem ser configurados no seu arquivo `.env` usando a variável `SEARCH_API`:
|
||||||
|
|
||||||
- **Tavily** (padrão): Uma API de busca especializada para aplicações de IA
|
- **Tavily** (padrão): Uma API de busca especializada para aplicações de IA
|
||||||
|
|
||||||
- Requer `TAVILY_API_KEY` no seu arquivo `.env`
|
- Requer `TAVILY_API_KEY` no seu arquivo `.env`
|
||||||
- Inscreva-se em: https://app.tavily.com/home
|
- Inscreva-se em: <https://app.tavily.com/home>
|
||||||
|
|
||||||
- **DuckDuckGo**: Mecanismo de busca focado em privacidade
|
- **DuckDuckGo**: Mecanismo de busca focado em privacidade
|
||||||
|
|
||||||
@@ -163,7 +162,7 @@ DeerFlow suporta múltiplos mecanismos de busca que podem ser configurados no se
|
|||||||
- **Brave Search**: Mecanismo de busca focado em privacidade com funcionalidades avançadas
|
- **Brave Search**: Mecanismo de busca focado em privacidade com funcionalidades avançadas
|
||||||
|
|
||||||
- Requer `BRAVE_SEARCH_API_KEY` no seu arquivo `.env`
|
- Requer `BRAVE_SEARCH_API_KEY` no seu arquivo `.env`
|
||||||
- Inscreva-se em: https://brave.com/search/api/
|
- Inscreva-se em: <https://brave.com/search/api/>
|
||||||
|
|
||||||
- **Arxiv**: Busca de artigos científicos para pesquisa acadêmica
|
- **Arxiv**: Busca de artigos científicos para pesquisa acadêmica
|
||||||
- Não requer chave API
|
- Não requer chave API
|
||||||
@@ -204,7 +203,6 @@ SEARCH_API=tavily
|
|||||||
|
|
||||||
- 🧠 **Humano-no-processo**
|
- 🧠 **Humano-no-processo**
|
||||||
|
|
||||||
|
|
||||||
- Suporta modificação interativa de planos de pesquisa usando linguagem natural
|
- Suporta modificação interativa de planos de pesquisa usando linguagem natural
|
||||||
- Suporta auto-aceite de planos de pesquisa
|
- Suporta auto-aceite de planos de pesquisa
|
||||||
|
|
||||||
@@ -333,9 +331,9 @@ langgraph dev
|
|||||||
|
|
||||||
Após iniciar o servidor LangGraph, você verá diversas URLs no seu terminal:
|
Após iniciar o servidor LangGraph, você verá diversas URLs no seu terminal:
|
||||||
|
|
||||||
- API: http://127.0.0.1:2024
|
- API: <http://127.0.0.1:2024>
|
||||||
- Studio UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
|
- Studio UI: <https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024>
|
||||||
- API Docs: http://127.0.0.1:2024/docs
|
- API Docs: <http://127.0.0.1:2024/docs>
|
||||||
|
|
||||||
Abra o link do Studio UI no seu navegador para acessar a interface de depuração.
|
Abra o link do Studio UI no seu navegador para acessar a interface de depuração.
|
||||||
|
|
||||||
@@ -343,7 +341,6 @@ Abra o link do Studio UI no seu navegador para acessar a interface de depuraçã
|
|||||||
|
|
||||||
No Studio UI, você pode:
|
No Studio UI, você pode:
|
||||||
|
|
||||||
|
|
||||||
1. Visualizar o grafo do fluxo de trabalho e como seus componentes se conectam
|
1. Visualizar o grafo do fluxo de trabalho e como seus componentes se conectam
|
||||||
2. Rastrear a execução em tempo-real e ver como os dados fluem através do sistema
|
2. Rastrear a execução em tempo-real e ver como os dados fluem através do sistema
|
||||||
3. Inspecionar o estado de cada passo do fluxo de trabalho
|
3. Inspecionar o estado de cada passo do fluxo de trabalho
|
||||||
@@ -391,7 +388,7 @@ docker compose build
|
|||||||
docker compose up
|
docker compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
## Exemplos:
|
## Exemplos
|
||||||
|
|
||||||
Os seguintes exemplos demonstram as capacidades do DeerFlow:
|
Os seguintes exemplos demonstram as capacidades do DeerFlow:
|
||||||
|
|
||||||
@@ -495,6 +492,7 @@ DeerFlow inclue um mecanismo de humano no processo que permite a você revisar,
|
|||||||
- Via API: Defina `auto_accepted_plan: true` na sua requisição
|
- Via API: Defina `auto_accepted_plan: true` na sua requisição
|
||||||
|
|
||||||
4. **Integração de API**: Quanto usar a API, você pode fornecer um feedback através do parâmetro `feedback`:
|
4. **Integração de API**: Quanto usar a API, você pode fornecer um feedback através do parâmetro `feedback`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"messages": [{ "role": "user", "content": "O que é computação quântica?" }],
|
"messages": [{ "role": "user", "content": "O que é computação quântica?" }],
|
||||||
|
|||||||
+9
-6
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
### Видео
|
### Видео
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/f3786598-1f2a-4d07-919e-8b99dfa1de3e
|
<https://github.com/user-attachments/assets/f3786598-1f2a-4d07-919e-8b99dfa1de3e>
|
||||||
|
|
||||||
В этой демонстрации мы показываем, как использовать DeerFlow для:
|
В этой демонстрации мы показываем, как использовать DeerFlow для:
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ DeerFlow поддерживает несколько поисковых сист
|
|||||||
- **Tavily** (по умолчанию): Специализированный поисковый API для приложений ИИ
|
- **Tavily** (по умолчанию): Специализированный поисковый API для приложений ИИ
|
||||||
|
|
||||||
- Требуется `TAVILY_API_KEY` в вашем файле `.env`
|
- Требуется `TAVILY_API_KEY` в вашем файле `.env`
|
||||||
- Зарегистрируйтесь на: https://app.tavily.com/home
|
- Зарегистрируйтесь на: <https://app.tavily.com/home>
|
||||||
|
|
||||||
- **DuckDuckGo**: Поисковая система, ориентированная на конфиденциальность
|
- **DuckDuckGo**: Поисковая система, ориентированная на конфиденциальность
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ DeerFlow поддерживает несколько поисковых сист
|
|||||||
- **Brave Search**: Поисковая система, ориентированная на конфиденциальность, с расширенными функциями
|
- **Brave Search**: Поисковая система, ориентированная на конфиденциальность, с расширенными функциями
|
||||||
|
|
||||||
- Требуется `BRAVE_SEARCH_API_KEY` в вашем файле `.env`
|
- Требуется `BRAVE_SEARCH_API_KEY` в вашем файле `.env`
|
||||||
- Зарегистрируйтесь на: https://brave.com/search/api/
|
- Зарегистрируйтесь на: <https://brave.com/search/api/>
|
||||||
|
|
||||||
- **Arxiv**: Поиск научных статей для академических исследований
|
- **Arxiv**: Поиск научных статей для академических исследований
|
||||||
- Не требуется API-ключ
|
- Не требуется API-ключ
|
||||||
@@ -325,9 +325,9 @@ langgraph dev
|
|||||||
|
|
||||||
После запуска сервера LangGraph вы увидите несколько URL в терминале:
|
После запуска сервера LangGraph вы увидите несколько URL в терминале:
|
||||||
|
|
||||||
- API: http://127.0.0.1:2024
|
- API: <http://127.0.0.1:2024>
|
||||||
- Studio UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
|
- Studio UI: <https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024>
|
||||||
- API Docs: http://127.0.0.1:2024/docs
|
- API Docs: <http://127.0.0.1:2024/docs>
|
||||||
|
|
||||||
Откройте ссылку Studio UI в вашем браузере для доступа к интерфейсу отладки.
|
Откройте ссылку Studio UI в вашем браузере для доступа к интерфейсу отладки.
|
||||||
|
|
||||||
@@ -353,6 +353,7 @@ langgraph dev
|
|||||||
DeerFlow поддерживает трассировку LangSmith, чтобы помочь вам отладить и контролировать ваши рабочие процессы. Чтобы включить трассировку LangSmith:
|
DeerFlow поддерживает трассировку LangSmith, чтобы помочь вам отладить и контролировать ваши рабочие процессы. Чтобы включить трассировку LangSmith:
|
||||||
|
|
||||||
1. Убедитесь, что в вашем файле `.env` есть следующие конфигурации (см. `.env.example`):
|
1. Убедитесь, что в вашем файле `.env` есть следующие конфигурации (см. `.env.example`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
LANGSMITH_TRACING=true
|
LANGSMITH_TRACING=true
|
||||||
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
|
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
|
||||||
@@ -361,6 +362,7 @@ DeerFlow поддерживает трассировку LangSmith, чтобы
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. Запустите трассировку и визуализируйте граф локально с LangSmith, выполнив:
|
2. Запустите трассировку и визуализируйте граф локально с LangSmith, выполнив:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
langgraph dev
|
langgraph dev
|
||||||
```
|
```
|
||||||
@@ -504,6 +506,7 @@ DeerFlow включает механизм "человек в контуре",
|
|||||||
- Через API: Установите `auto_accepted_plan: true` в вашем запросе
|
- Через API: Установите `auto_accepted_plan: true` в вашем запросе
|
||||||
|
|
||||||
4. **Интеграция API**: При использовании API вы можете предоставить обратную связь через параметр `feedback`:
|
4. **Интеграция API**: При использовании API вы можете предоставить обратную связь через параметр `feedback`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"messages": [{ "role": "user", "content": "Что такое квантовые вычисления?" }],
|
"messages": [{ "role": "user", "content": "Что такое квантовые вычисления?" }],
|
||||||
|
|||||||
+69
-28
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
**DeerFlow**(**D**eep **E**xploration and **E**fficient **R**esearch **Flow**)是一个社区驱动的深度研究框架,它建立在开源社区的杰出工作基础之上。我们的目标是将语言模型与专业工具(如网络搜索、爬虫和 Python 代码执行)相结合,同时回馈使这一切成为可能的社区。
|
**DeerFlow**(**D**eep **E**xploration and **E**fficient **R**esearch **Flow**)是一个社区驱动的深度研究框架,它建立在开源社区的杰出工作基础之上。我们的目标是将语言模型与专业工具(如网络搜索、爬虫和 Python 代码执行)相结合,同时回馈使这一切成为可能的社区。
|
||||||
|
|
||||||
目前,DeerFlow 已正式入驻火山引擎的 FaaS 应用中心,用户可通过体验链接进行在线体验,直观感受其强大功能与便捷操作;同时,为满足不同用户的部署需求,DeerFlow 支持基于火山引擎一键部署,点击部署链接即可快速完成部署流程,开启高效研究之旅。
|
目前,DeerFlow 已正式入驻[火山引擎的 FaaS 应用中心](https://console.volcengine.com/vefaas/region:vefaas+cn-beijing/market),用户可通过[体验链接](https://console.volcengine.com/vefaas/region:vefaas+cn-beijing/market/deerflow/?channel=github&source=deerflow)进行在线体验,直观感受其强大功能与便捷操作;同时,为满足不同用户的部署需求,DeerFlow 支持基于火山引擎一键部署,点击[部署链接](https://console.volcengine.com/vefaas/region:vefaas+cn-beijing/application/create?templateId=683adf9e372daa0008aaed5c&channel=github&source=deerflow)即可快速完成部署流程,开启高效研究之旅。
|
||||||
|
|
||||||
请访问[DeerFlow 的官方网站](https://deerflow.tech/)了解更多详情。
|
请访问[DeerFlow 的官方网站](https://deerflow.tech/)了解更多详情。
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
### 视频
|
### 视频
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/f3786598-1f2a-4d07-919e-8b99dfa1de3e
|
<https://github.com/user-attachments/assets/f3786598-1f2a-4d07-919e-8b99dfa1de3e>
|
||||||
|
|
||||||
在此演示中,我们展示了如何使用 DeerFlow:
|
在此演示中,我们展示了如何使用 DeerFlow:
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ https://github.com/user-attachments/assets/f3786598-1f2a-4d07-919e-8b99dfa1de3e
|
|||||||
- [❓ 常见问题](#常见问题)
|
- [❓ 常见问题](#常见问题)
|
||||||
- [📜 许可证](#许可证)
|
- [📜 许可证](#许可证)
|
||||||
- [💖 致谢](#致谢)
|
- [💖 致谢](#致谢)
|
||||||
- [⭐ Star History](#star-History)
|
- [⭐ Star History](#star-history)
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ pnpm install
|
|||||||
|
|
||||||
请参阅[配置指南](docs/configuration_guide.md)获取更多详情。
|
请参阅[配置指南](docs/configuration_guide.md)获取更多详情。
|
||||||
|
|
||||||
> [!注意]
|
> [! 注意]
|
||||||
> 在启动项目之前,请仔细阅读指南,并更新配置以匹配您的特定设置和要求。
|
> 在启动项目之前,请仔细阅读指南,并更新配置以匹配您的特定设置和要求。
|
||||||
|
|
||||||
### 控制台 UI
|
### 控制台 UI
|
||||||
@@ -123,8 +123,7 @@ uv run main.py
|
|||||||
### Web UI
|
### Web UI
|
||||||
|
|
||||||
本项目还包括一个 Web UI,提供更加动态和引人入胜的交互体验。
|
本项目还包括一个 Web UI,提供更加动态和引人入胜的交互体验。
|
||||||
|
> [! 注意]
|
||||||
> [!注意]
|
|
||||||
> 您需要先安装 Web UI 的依赖。
|
> 您需要先安装 Web UI 的依赖。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -142,21 +141,20 @@ bootstrap.bat -d
|
|||||||
|
|
||||||
## 支持的搜索引擎
|
## 支持的搜索引擎
|
||||||
|
|
||||||
|
### 公域搜索引擎
|
||||||
|
|
||||||
DeerFlow 支持多种搜索引擎,可以在`.env`文件中通过`SEARCH_API`变量进行配置:
|
DeerFlow 支持多种搜索引擎,可以在`.env`文件中通过`SEARCH_API`变量进行配置:
|
||||||
|
|
||||||
- **Tavily**(默认):专为 AI 应用设计的专业搜索 API
|
- **Tavily**(默认):专为 AI 应用设计的专业搜索 API
|
||||||
|
|
||||||
- 需要在`.env`文件中设置`TAVILY_API_KEY`
|
- 需要在`.env`文件中设置`TAVILY_API_KEY`
|
||||||
- 注册地址:https://app.tavily.com/home
|
- 注册地址:<https://app.tavily.com/home>
|
||||||
|
|
||||||
- **DuckDuckGo**:注重隐私的搜索引擎
|
- **DuckDuckGo**:注重隐私的搜索引擎
|
||||||
|
|
||||||
- 无需 API 密钥
|
- 无需 API 密钥
|
||||||
|
|
||||||
- **Brave Search**:具有高级功能的注重隐私的搜索引擎
|
- **Brave Search**:具有高级功能的注重隐私的搜索引擎
|
||||||
|
|
||||||
- 需要在`.env`文件中设置`BRAVE_SEARCH_API_KEY`
|
- 需要在`.env`文件中设置`BRAVE_SEARCH_API_KEY`
|
||||||
- 注册地址:https://brave.com/search/api/
|
- 注册地址:<https://brave.com/search/api/>
|
||||||
|
|
||||||
- **Arxiv**:用于学术研究的科学论文搜索
|
- **Arxiv**:用于学术研究的科学论文搜索
|
||||||
- 无需 API 密钥
|
- 无需 API 密钥
|
||||||
@@ -169,6 +167,30 @@ DeerFlow 支持多种搜索引擎,可以在`.env`文件中通过`SEARCH_API`
|
|||||||
SEARCH_API=tavily
|
SEARCH_API=tavily
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 私域知识库引擎
|
||||||
|
|
||||||
|
DeerFlow 支持基于私有域知识的检索,您可以将文档上传到多种私有知识库中,以便在研究过程中使用,当前支持的私域知识库有:
|
||||||
|
|
||||||
|
- **[RAGFlow](https://ragflow.io/docs/dev/)**:开源的基于检索增强生成的知识库引擎
|
||||||
|
```
|
||||||
|
# 参照示例进行配置 .env.example
|
||||||
|
RAG_PROVIDER=ragflow
|
||||||
|
RAGFLOW_API_URL="http://localhost:9388"
|
||||||
|
RAGFLOW_API_KEY="ragflow-xxx"
|
||||||
|
RAGFLOW_RETRIEVAL_SIZE=10
|
||||||
|
```
|
||||||
|
|
||||||
|
- **[VikingDB 知识库](https://www.volcengine.com/docs/84313/1254457)**:火山引擎提供的公有云知识库引擎
|
||||||
|
> 注意先从 [火山引擎](https://www.volcengine.com/docs/84313/1254485) 获取账号 AK/SK
|
||||||
|
```
|
||||||
|
# 参照示例进行配置 .env.example
|
||||||
|
RAG_PROVIDER=vikingdb_knowledge_base
|
||||||
|
VIKINGDB_KNOWLEDGE_BASE_API_URL="api-knowledgebase.mlp.cn-beijing.volces.com"
|
||||||
|
VIKINGDB_KNOWLEDGE_BASE_API_AK="volcengine-ak-xxx"
|
||||||
|
VIKINGDB_KNOWLEDGE_BASE_API_SK="volcengine-sk-xxx"
|
||||||
|
VIKINGDB_KNOWLEDGE_BASE_RETRIEVAL_SIZE=15
|
||||||
|
```
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
### 核心能力
|
### 核心能力
|
||||||
@@ -182,10 +204,14 @@ SEARCH_API=tavily
|
|||||||
### 工具和 MCP 集成
|
### 工具和 MCP 集成
|
||||||
|
|
||||||
- 🔍 **搜索和检索**
|
- 🔍 **搜索和检索**
|
||||||
|
|
||||||
- 通过 Tavily、Brave Search 等进行网络搜索
|
- 通过 Tavily、Brave Search 等进行网络搜索
|
||||||
- 使用 Jina 进行爬取
|
- 使用 Jina 进行爬取
|
||||||
- 高级内容提取
|
- 高级内容提取
|
||||||
|
- 支持检索指定私有知识库
|
||||||
|
|
||||||
|
- 📃 **RAG 集成**
|
||||||
|
- 支持 [RAGFlow](https://github.com/infiniflow/ragflow) 知识库
|
||||||
|
- 支持 [VikingDB](https://www.volcengine.com/docs/84313/1254457) 火山知识库
|
||||||
|
|
||||||
- 🔗 **MCP 无缝集成**
|
- 🔗 **MCP 无缝集成**
|
||||||
- 扩展私有域访问、知识图谱、网页浏览等能力
|
- 扩展私有域访问、知识图谱、网页浏览等能力
|
||||||
@@ -194,7 +220,6 @@ SEARCH_API=tavily
|
|||||||
### 人机协作
|
### 人机协作
|
||||||
|
|
||||||
- 🧠 **人在环中**
|
- 🧠 **人在环中**
|
||||||
|
|
||||||
- 支持使用自然语言交互式修改研究计划
|
- 支持使用自然语言交互式修改研究计划
|
||||||
- 支持自动接受研究计划
|
- 支持自动接受研究计划
|
||||||
|
|
||||||
@@ -233,16 +258,36 @@ DeerFlow 实现了一个模块化的多智能体系统架构,专为自动化
|
|||||||
- 管理研究流程并决定何时生成最终报告
|
- 管理研究流程并决定何时生成最终报告
|
||||||
|
|
||||||
3. **研究团队**:执行计划的专业智能体集合:
|
3. **研究团队**:执行计划的专业智能体集合:
|
||||||
|
|
||||||
- **研究员**:使用网络搜索引擎、爬虫甚至 MCP 服务等工具进行网络搜索和信息收集。
|
- **研究员**:使用网络搜索引擎、爬虫甚至 MCP 服务等工具进行网络搜索和信息收集。
|
||||||
- **编码员**:使用 Python REPL 工具处理代码分析、执行和技术任务。
|
- **编码员**:使用 Python REPL 工具处理代码分析、执行和技术任务。
|
||||||
每个智能体都可以访问针对其角色优化的特定工具,并在 LangGraph 框架内运行
|
每个智能体都可以访问针对其角色优化的特定工具,并在 LangGraph 框架内运行
|
||||||
|
|
||||||
4. **报告员**:研究输出的最终阶段处理器
|
4. **报告员**:研究输出的最终阶段处理器
|
||||||
- 汇总研究团队的发现
|
- 汇总研究团队的发现
|
||||||
- 处理和组织收集的信息
|
- 处理和组织收集的信息
|
||||||
- 生成全面的研究报告
|
- 生成全面的研究报告
|
||||||
|
|
||||||
|
## 文本转语音集成
|
||||||
|
|
||||||
|
DeerFlow 现在包含一个文本转语音 (TTS) 功能,允许您将研究报告转换为语音。此功能使用火山引擎 TTS API 生成高质量的文本音频。速度、音量和音调等特性也可以自定义。
|
||||||
|
|
||||||
|
### 使用 TTS API
|
||||||
|
|
||||||
|
您可以通过`/api/tts`端点访问 TTS 功能:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用curl的API调用示例
|
||||||
|
curl --location 'http://localhost:8000/api/tts' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"text": "这是文本转语音功能的测试。",
|
||||||
|
"speed_ratio": 1.0,
|
||||||
|
"volume_ratio": 1.0,
|
||||||
|
"pitch_ratio": 1.0
|
||||||
|
}' \
|
||||||
|
--output speech.mp3
|
||||||
|
```
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
### 测试
|
### 测试
|
||||||
@@ -301,9 +346,9 @@ langgraph dev
|
|||||||
|
|
||||||
启动 LangGraph 服务器后,您将在终端中看到几个 URL:
|
启动 LangGraph 服务器后,您将在终端中看到几个 URL:
|
||||||
|
|
||||||
- API: http://127.0.0.1:2024
|
- API: <http://127.0.0.1:2024>
|
||||||
- Studio UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
|
- Studio UI: <https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024>
|
||||||
- API 文档: http://127.0.0.1:2024/docs
|
- API 文档:<http://127.0.0.1:2024/docs>
|
||||||
|
|
||||||
在浏览器中打开 Studio UI 链接以访问调试界面。
|
在浏览器中打开 Studio UI 链接以访问调试界面。
|
||||||
|
|
||||||
@@ -329,6 +374,7 @@ langgraph dev
|
|||||||
DeerFlow 支持 LangSmith 追踪功能,帮助您调试和监控工作流。要启用 LangSmith 追踪:
|
DeerFlow 支持 LangSmith 追踪功能,帮助您调试和监控工作流。要启用 LangSmith 追踪:
|
||||||
|
|
||||||
1. 确保您的 `.env` 文件中有以下配置(参见 `.env.example`):
|
1. 确保您的 `.env` 文件中有以下配置(参见 `.env.example`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
LANGSMITH_TRACING=true
|
LANGSMITH_TRACING=true
|
||||||
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
|
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
|
||||||
@@ -337,6 +383,7 @@ DeerFlow 支持 LangSmith 追踪功能,帮助您调试和监控工作流。要
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. 通过运行以下命令本地启动 LangSmith 追踪:
|
2. 通过运行以下命令本地启动 LangSmith 追踪:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
langgraph dev
|
langgraph dev
|
||||||
```
|
```
|
||||||
@@ -379,7 +426,7 @@ docker compose up
|
|||||||
|
|
||||||
## 文本转语音集成
|
## 文本转语音集成
|
||||||
|
|
||||||
DeerFlow 现在包含一个文本转语音(TTS)功能,允许您将研究报告转换为语音。此功能使用火山引擎 TTS API 生成高质量的文本音频。速度、音量和音调等特性也可以自定义。
|
DeerFlow 现在包含一个文本转语音 (TTS) 功能,允许您将研究报告转换为语音。此功能使用火山引擎 TTS API 生成高质量的文本音频。速度、音量和音调等特性也可以自定义。
|
||||||
|
|
||||||
### 使用 TTS API
|
### 使用 TTS API
|
||||||
|
|
||||||
@@ -405,17 +452,14 @@ curl --location 'http://localhost:8000/api/tts' \
|
|||||||
### 研究报告
|
### 研究报告
|
||||||
|
|
||||||
1. **OpenAI Sora 报告** - OpenAI 的 Sora AI 工具分析
|
1. **OpenAI Sora 报告** - OpenAI 的 Sora AI 工具分析
|
||||||
|
|
||||||
- 讨论功能、访问方式、提示工程、限制和伦理考虑
|
- 讨论功能、访问方式、提示工程、限制和伦理考虑
|
||||||
- [查看完整报告](examples/openai_sora_report.md)
|
- [查看完整报告](examples/openai_sora_report.md)
|
||||||
|
|
||||||
2. **Google 的 Agent to Agent 协议报告** - Google 的 Agent to Agent (A2A)协议概述
|
2. **Google 的 Agent to Agent 协议报告** - Google 的 Agent to Agent (A2A) 协议概述
|
||||||
|
- 讨论其在 AI 智能体通信中的作用及其与 Anthropic 的 Model Context Protocol (MCP) 的关系
|
||||||
- 讨论其在 AI 智能体通信中的作用及其与 Anthropic 的 Model Context Protocol (MCP)的关系
|
|
||||||
- [查看完整报告](examples/what_is_agent_to_agent_protocol.md)
|
- [查看完整报告](examples/what_is_agent_to_agent_protocol.md)
|
||||||
|
|
||||||
3. **什么是 MCP?** - 对"MCP"一词在多个上下文中的全面分析
|
3. **什么是 MCP?** - 对"MCP"一词在多个上下文中的全面分析
|
||||||
|
|
||||||
- 探讨 AI 中的 Model Context Protocol、化学中的 Monocalcium Phosphate 和电子学中的 Micro-channel Plate
|
- 探讨 AI 中的 Model Context Protocol、化学中的 Monocalcium Phosphate 和电子学中的 Micro-channel Plate
|
||||||
- [查看完整报告](examples/what_is_mcp.md)
|
- [查看完整报告](examples/what_is_mcp.md)
|
||||||
|
|
||||||
@@ -426,17 +470,14 @@ curl --location 'http://localhost:8000/api/tts' \
|
|||||||
- [查看完整报告](examples/bitcoin_price_fluctuation.md)
|
- [查看完整报告](examples/bitcoin_price_fluctuation.md)
|
||||||
|
|
||||||
5. **什么是 LLM?** - 对大型语言模型的深入探索
|
5. **什么是 LLM?** - 对大型语言模型的深入探索
|
||||||
|
|
||||||
- 讨论架构、训练、应用和伦理考虑
|
- 讨论架构、训练、应用和伦理考虑
|
||||||
- [查看完整报告](examples/what_is_llm.md)
|
- [查看完整报告](examples/what_is_llm.md)
|
||||||
|
|
||||||
6. **如何使用 Claude 进行深度研究?** - 在深度研究中使用 Claude 的最佳实践和工作流程
|
6. **如何使用 Claude 进行深度研究?** - 在深度研究中使用 Claude 的最佳实践和工作流程
|
||||||
|
|
||||||
- 涵盖提示工程、数据分析和与其他工具的集成
|
- 涵盖提示工程、数据分析和与其他工具的集成
|
||||||
- [查看完整报告](examples/how_to_use_claude_deep_research.md)
|
- [查看完整报告](examples/how_to_use_claude_deep_research.md)
|
||||||
|
|
||||||
7. **医疗保健中的 AI 采用:影响因素** - 影响医疗保健中 AI 采用的因素分析
|
7. **医疗保健中的 AI 采用:影响因素** - 影响医疗保健中 AI 采用的因素分析
|
||||||
|
|
||||||
- 讨论 AI 技术、数据质量、伦理考虑、经济评估、组织准备度和数字基础设施
|
- 讨论 AI 技术、数据质量、伦理考虑、经济评估、组织准备度和数字基础设施
|
||||||
- [查看完整报告](examples/AI_adoption_in_healthcare.md)
|
- [查看完整报告](examples/AI_adoption_in_healthcare.md)
|
||||||
|
|
||||||
@@ -497,10 +538,10 @@ DeerFlow 包含一个人在环中机制,允许您在执行研究计划前审
|
|||||||
- 系统将整合您的反馈并生成修订后的计划
|
- 系统将整合您的反馈并生成修订后的计划
|
||||||
|
|
||||||
3. **自动接受**:您可以启用自动接受以跳过审查过程:
|
3. **自动接受**:您可以启用自动接受以跳过审查过程:
|
||||||
|
|
||||||
- 通过 API:在请求中设置`auto_accepted_plan: true`
|
- 通过 API:在请求中设置`auto_accepted_plan: true`
|
||||||
|
|
||||||
4. **API 集成**:使用 API 时,您可以通过`feedback`参数提供反馈:
|
4. **API 集成**:使用 API 时,您可以通过`feedback`参数提供反馈:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"messages": [{ "role": "user", "content": "什么是量子计算?" }],
|
"messages": [{ "role": "user", "content": "什么是量子计算?" }],
|
||||||
|
|||||||
+21
-1
@@ -3,18 +3,38 @@
|
|||||||
# configurations to match your specific settings and requirements.
|
# configurations to match your specific settings and requirements.
|
||||||
# - Replace `api_key` with your own credentials.
|
# - Replace `api_key` with your own credentials.
|
||||||
# - Replace `base_url` and `model` name if you want to use a custom model.
|
# - Replace `base_url` and `model` name if you want to use a custom model.
|
||||||
|
# - Set `verify_ssl` to `false` if your LLM server uses self-signed certificates
|
||||||
# - A restart is required every time you change the `config.yaml` file.
|
# - A restart is required every time you change the `config.yaml` file.
|
||||||
|
|
||||||
BASIC_MODEL:
|
BASIC_MODEL:
|
||||||
base_url: https://ark.cn-beijing.volces.com/api/v3
|
base_url: https://ark.cn-beijing.volces.com/api/v3
|
||||||
model: "doubao-1-5-pro-32k-250115"
|
model: "doubao-1-5-pro-32k-250115"
|
||||||
api_key: xxxx
|
api_key: xxxx
|
||||||
|
# max_retries: 3 # Maximum number of retries for LLM calls
|
||||||
|
# verify_ssl: false # Uncomment this line to disable SSL certificate verification for self-signed certificates
|
||||||
|
|
||||||
# Reasoning model is optional.
|
# Reasoning model is optional.
|
||||||
# Uncomment the following settings if you want to use reasoning model
|
# Uncomment the following settings if you want to use reasoning model
|
||||||
# for planning.
|
# for planning.
|
||||||
|
|
||||||
# REASONING_MODEL:
|
# REASONING_MODEL:
|
||||||
# base_url: https://ark-cn-beijing.bytedance.net/api/v3
|
# base_url: https://ark.cn-beijing.volces.com/api/v3
|
||||||
# model: "doubao-1-5-thinking-pro-m-250428"
|
# model: "doubao-1-5-thinking-pro-m-250428"
|
||||||
# api_key: xxxx
|
# api_key: xxxx
|
||||||
|
# max_retries: 3 # Maximum number of retries for LLM calls
|
||||||
|
|
||||||
|
|
||||||
|
# OTHER SETTINGS:
|
||||||
|
# Search engine configuration (Only supports Tavily currently)
|
||||||
|
# SEARCH_ENGINE:
|
||||||
|
# engine: tavily
|
||||||
|
# # Only include results from these domains
|
||||||
|
# include_domains:
|
||||||
|
# - example.com
|
||||||
|
# - trusted-news.com
|
||||||
|
# - reliable-source.org
|
||||||
|
# - gov.cn
|
||||||
|
# - edu.cn
|
||||||
|
# # Exclude results from these domains
|
||||||
|
# exclude_domains:
|
||||||
|
# - example.com
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
volumes:
|
volumes:
|
||||||
- ./conf.yaml:/app/conf.yaml
|
- ./conf.yaml:/app/conf.yaml:ro
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- deer-flow-network
|
- deer-flow-network
|
||||||
|
|||||||
+52
-11
@@ -11,7 +11,7 @@ cp conf.yaml.example conf.yaml
|
|||||||
|
|
||||||
## Which models does DeerFlow support?
|
## Which models does DeerFlow support?
|
||||||
|
|
||||||
In DeerFlow, currently we only support non-reasoning models, which means models like OpenAI's o1/o3 or DeepSeek's R1 are not supported yet, but we will add support for them in the future.
|
In DeerFlow, we currently only support non-reasoning models. This means models like OpenAI's o1/o3 or DeepSeek's R1 are not supported yet, but we plan to add support for them in the future. Additionally, all Gemma-3 models are currently unsupported due to the lack of tool usage capabilities.
|
||||||
|
|
||||||
### Supported Models
|
### Supported Models
|
||||||
|
|
||||||
@@ -58,15 +58,31 @@ BASIC_MODEL:
|
|||||||
api_key: YOUR_API_KEY
|
api_key: YOUR_API_KEY
|
||||||
```
|
```
|
||||||
|
|
||||||
### How to use Ollama models?
|
### How to use models with self-signed SSL certificates?
|
||||||
|
|
||||||
DeerFlow supports the integration of Ollama models. You can refer to [litellm Ollama](https://docs.litellm.ai/docs/providers/ollama). <br>
|
If your LLM server uses self-signed SSL certificates, you can disable SSL certificate verification by adding the `verify_ssl: false` parameter to your model configuration:
|
||||||
The following is a configuration example of `conf.yaml` for using Ollama models:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
BASIC_MODEL:
|
BASIC_MODEL:
|
||||||
model: "ollama/ollama-model-name"
|
base_url: "https://your-llm-server.com/api/v1"
|
||||||
base_url: "http://localhost:11434" # Local service address of Ollama, which can be started/viewed via ollama serve
|
model: "your-model-name"
|
||||||
|
api_key: YOUR_API_KEY
|
||||||
|
verify_ssl: false # Disable SSL certificate verification for self-signed certificates
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Disabling SSL certificate verification reduces security and should only be used in development environments or when you trust the LLM server. In production environments, it's recommended to use properly signed SSL certificates.
|
||||||
|
|
||||||
|
### How to use Ollama models?
|
||||||
|
|
||||||
|
DeerFlow supports the integration of Ollama models. You can refer to [litellm Ollama](https://docs.litellm.ai/docs/providers/ollama). <br>
|
||||||
|
The following is a configuration example of `conf.yaml` for using Ollama models(you might need to run the 'ollama serve' first):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
BASIC_MODEL:
|
||||||
|
model: "model-name" # Model name, which supports the completions API(important), such as: qwen3:8b, mistral-small3.1:24b, qwen2.5:3b
|
||||||
|
base_url: "http://localhost:11434/v1" # Local service address of Ollama, which can be started/viewed via ollama serve
|
||||||
|
api_key: "whatever" # Mandatory, fake api_key with a random string you like :-)
|
||||||
```
|
```
|
||||||
|
|
||||||
### How to use OpenRouter models?
|
### How to use OpenRouter models?
|
||||||
@@ -89,13 +105,38 @@ BASIC_MODEL:
|
|||||||
|
|
||||||
Note: The available models and their exact names may change over time. Please verify the currently available models and their correct identifiers in [OpenRouter's official documentation](https://openrouter.ai/docs).
|
Note: The available models and their exact names may change over time. Please verify the currently available models and their correct identifiers in [OpenRouter's official documentation](https://openrouter.ai/docs).
|
||||||
|
|
||||||
### How to use Azure models?
|
|
||||||
|
|
||||||
DeerFlow supports the integration of Azure models. You can refer to [litellm Azure](https://docs.litellm.ai/docs/providers/azure). Configuration example of `conf.yaml`:
|
### How to use Azure OpenAI chat models?
|
||||||
|
|
||||||
|
DeerFlow supports the integration of Azure OpenAI chat models. You can refer to [AzureChatOpenAI](https://python.langchain.com/api_reference/openai/chat_models/langchain_openai.chat_models.azure.AzureChatOpenAI.html). Configuration example of `conf.yaml`:
|
||||||
```yaml
|
```yaml
|
||||||
BASIC_MODEL:
|
BASIC_MODEL:
|
||||||
model: "azure/gpt-4o-2024-08-06"
|
model: "azure/gpt-4o-2024-08-06"
|
||||||
api_base: $AZURE_API_BASE
|
azure_endpoint: $AZURE_OPENAI_ENDPOINT
|
||||||
api_version: $AZURE_API_VERSION
|
api_version: $OPENAI_API_VERSION
|
||||||
api_key: $AZURE_API_KEY
|
api_key: $AZURE_OPENAI_API_KEY
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## About Search Engine
|
||||||
|
|
||||||
|
### How to control search domains for Tavily?
|
||||||
|
|
||||||
|
DeerFlow allows you to control which domains are included or excluded in Tavily search results through the configuration file. This helps improve search result quality and reduce hallucinations by focusing on trusted sources.
|
||||||
|
|
||||||
|
`Tips`: it only supports Tavily currently.
|
||||||
|
|
||||||
|
You can configure domain filtering in your `conf.yaml` file as follows:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
SEARCH_ENGINE:
|
||||||
|
engine: tavily
|
||||||
|
# Only include results from these domains (whitelist)
|
||||||
|
include_domains:
|
||||||
|
- trusted-news.com
|
||||||
|
- gov.org
|
||||||
|
- reliable-source.edu
|
||||||
|
# Exclude results from these domains (blacklist)
|
||||||
|
exclude_domains:
|
||||||
|
- unreliable-site.com
|
||||||
|
- spam-domain.net
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,11 @@ if __name__ == "__main__":
|
|||||||
if args.query:
|
if args.query:
|
||||||
user_query = " ".join(args.query)
|
user_query = " ".join(args.query)
|
||||||
else:
|
else:
|
||||||
user_query = input("Enter your query: ")
|
# Loop until user provides non-empty input
|
||||||
|
while True:
|
||||||
|
user_query = input("Enter your query: ")
|
||||||
|
if user_query is not None and user_query != "":
|
||||||
|
break
|
||||||
|
|
||||||
# Run the agent workflow with the provided parameters
|
# Run the agent workflow with the provided parameters
|
||||||
ask(
|
ask(
|
||||||
|
|||||||
@@ -33,18 +33,24 @@ dependencies = [
|
|||||||
"mcp>=1.6.0",
|
"mcp>=1.6.0",
|
||||||
"langchain-mcp-adapters>=0.0.9",
|
"langchain-mcp-adapters>=0.0.9",
|
||||||
"langchain-deepseek>=0.1.3",
|
"langchain-deepseek>=0.1.3",
|
||||||
|
"volcengine>=1.0.191",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
"ruff",
|
||||||
"black>=24.2.0",
|
"black>=24.2.0",
|
||||||
"langgraph-cli[inmem]>=0.2.10",
|
"langgraph-cli[inmem]>=0.2.10",
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
"pytest>=7.4.0",
|
"pytest>=7.4.0",
|
||||||
"pytest-cov>=4.1.0",
|
"pytest-cov>=4.1.0",
|
||||||
|
"pytest-asyncio>=1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
required-version = ">=0.6.15"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
python_files = ["test_*.py"]
|
python_files = ["test_*.py"]
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
from .tools import SELECTED_SEARCH_ENGINE, SearchEngine
|
|
||||||
from .loader import load_yaml_config
|
from .loader import load_yaml_config
|
||||||
|
from .tools import SELECTED_SEARCH_ENGINE, SearchEngine
|
||||||
from .questions import BUILT_IN_QUESTIONS, BUILT_IN_QUESTIONS_ZH_CN
|
from .questions import BUILT_IN_QUESTIONS, BUILT_IN_QUESTIONS_ZH_CN
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
@@ -11,7 +11,7 @@ from dotenv import load_dotenv
|
|||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# Team configuration
|
# Team configuration
|
||||||
TEAM_MEMBER_CONFIGRATIONS = {
|
TEAM_MEMBER_CONFIGURATIONS = {
|
||||||
"researcher": {
|
"researcher": {
|
||||||
"name": "researcher",
|
"name": "researcher",
|
||||||
"desc": (
|
"desc": (
|
||||||
@@ -36,14 +36,15 @@ TEAM_MEMBER_CONFIGRATIONS = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
TEAM_MEMBERS = list(TEAM_MEMBER_CONFIGRATIONS.keys())
|
TEAM_MEMBERS = list(TEAM_MEMBER_CONFIGURATIONS.keys())
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Other configurations
|
# Other configurations
|
||||||
"TEAM_MEMBERS",
|
"TEAM_MEMBERS",
|
||||||
"TEAM_MEMBER_CONFIGRATIONS",
|
"TEAM_MEMBER_CONFIGURATIONS",
|
||||||
"SELECTED_SEARCH_ENGINE",
|
"SELECTED_SEARCH_ENGINE",
|
||||||
"SearchEngine",
|
"SearchEngine",
|
||||||
"BUILT_IN_QUESTIONS",
|
"BUILT_IN_QUESTIONS",
|
||||||
"BUILT_IN_QUESTIONS_ZH_CN",
|
"BUILT_IN_QUESTIONS_ZH_CN",
|
||||||
|
load_yaml_config,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ SELECTED_SEARCH_ENGINE = os.getenv("SEARCH_API", SearchEngine.TAVILY.value)
|
|||||||
|
|
||||||
class RAGProvider(enum.Enum):
|
class RAGProvider(enum.Enum):
|
||||||
RAGFLOW = "ragflow"
|
RAGFLOW = "ragflow"
|
||||||
|
VIKINGDB_KNOWLEDGE_BASE = "vikingdb_knowledge_base"
|
||||||
|
|
||||||
|
|
||||||
SELECTED_RAG_PROVIDER = os.getenv("RAG_PROVIDER")
|
SELECTED_RAG_PROVIDER = os.getenv("RAG_PROVIDER")
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from .article import Article
|
from .article import Article
|
||||||
from .jina_client import JinaClient
|
from .jina_client import JinaClient
|
||||||
|
|||||||
+11
-2
@@ -22,14 +22,23 @@ def continue_to_running_research_team(state: State):
|
|||||||
current_plan = state.get("current_plan")
|
current_plan = state.get("current_plan")
|
||||||
if not current_plan or not current_plan.steps:
|
if not current_plan or not current_plan.steps:
|
||||||
return "planner"
|
return "planner"
|
||||||
|
|
||||||
if all(step.execution_res for step in current_plan.steps):
|
if all(step.execution_res for step in current_plan.steps):
|
||||||
return "planner"
|
return "planner"
|
||||||
|
|
||||||
|
# Find first incomplete step
|
||||||
|
incomplete_step = None
|
||||||
for step in current_plan.steps:
|
for step in current_plan.steps:
|
||||||
if not step.execution_res:
|
if not step.execution_res:
|
||||||
|
incomplete_step = step
|
||||||
break
|
break
|
||||||
if step.step_type and step.step_type == StepType.RESEARCH:
|
|
||||||
|
if not incomplete_step:
|
||||||
|
return "planner"
|
||||||
|
|
||||||
|
if incomplete_step.step_type == StepType.RESEARCH:
|
||||||
return "researcher"
|
return "researcher"
|
||||||
if step.step_type and step.step_type == StepType.PROCESSING:
|
if incomplete_step.step_type == StepType.PROCESSING:
|
||||||
return "coder"
|
return "coder"
|
||||||
return "planner"
|
return "planner"
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -134,7 +134,7 @@ def planner_node(
|
|||||||
return Command(goto="reporter")
|
return Command(goto="reporter")
|
||||||
else:
|
else:
|
||||||
return Command(goto="__end__")
|
return Command(goto="__end__")
|
||||||
if curr_plan.get("has_enough_context"):
|
if isinstance(curr_plan, dict) and curr_plan.get("has_enough_context"):
|
||||||
logger.info("Planner response has enough context.")
|
logger.info("Planner response has enough context.")
|
||||||
new_plan = Plan.model_validate(curr_plan)
|
new_plan = Plan.model_validate(curr_plan)
|
||||||
return Command(
|
return Command(
|
||||||
@@ -186,11 +186,9 @@ def human_feedback_node(
|
|||||||
plan_iterations += 1
|
plan_iterations += 1
|
||||||
# parse the plan
|
# parse the plan
|
||||||
new_plan = json.loads(current_plan)
|
new_plan = json.loads(current_plan)
|
||||||
if new_plan["has_enough_context"]:
|
|
||||||
goto = "reporter"
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
logger.warning("Planner response is not a valid JSON")
|
logger.warning("Planner response is not a valid JSON")
|
||||||
if plan_iterations > 0:
|
if plan_iterations > 1: # the plan_iterations is increased before this check
|
||||||
return Command(goto="reporter")
|
return Command(goto="reporter")
|
||||||
else:
|
else:
|
||||||
return Command(goto="__end__")
|
return Command(goto="__end__")
|
||||||
@@ -245,9 +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}")
|
||||||
|
messages = state.get("messages", [])
|
||||||
|
if response.content:
|
||||||
|
messages.append(HumanMessage(content=response.content, name="coordinator"))
|
||||||
return Command(
|
return Command(
|
||||||
update={
|
update={
|
||||||
|
"messages": messages,
|
||||||
"locale": locale,
|
"locale": locale,
|
||||||
"research_topic": research_topic,
|
"research_topic": research_topic,
|
||||||
"resources": configurable.resources,
|
"resources": configurable.resources,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
|
||||||
from langgraph.graph import MessagesState
|
from langgraph.graph import MessagesState
|
||||||
|
|
||||||
from src.prompts.planner_model import Plan
|
from src.prompts.planner_model import Plan
|
||||||
|
|||||||
+26
-17
@@ -4,8 +4,10 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
import os
|
import os
|
||||||
|
import httpx
|
||||||
|
|
||||||
from langchain_openai import ChatOpenAI
|
from langchain_core.language_models import BaseChatModel
|
||||||
|
from langchain_openai import ChatOpenAI, AzureChatOpenAI
|
||||||
from langchain_deepseek import ChatDeepSeek
|
from langchain_deepseek import ChatDeepSeek
|
||||||
from typing import get_args
|
from typing import get_args
|
||||||
|
|
||||||
@@ -13,7 +15,7 @@ from src.config import load_yaml_config
|
|||||||
from src.config.agents import LLMType
|
from src.config.agents import LLMType
|
||||||
|
|
||||||
# Cache for LLM instances
|
# Cache for LLM instances
|
||||||
_llm_cache: dict[LLMType, ChatOpenAI] = {}
|
_llm_cache: dict[LLMType, BaseChatModel] = {}
|
||||||
|
|
||||||
|
|
||||||
def _get_config_file_path() -> str:
|
def _get_config_file_path() -> str:
|
||||||
@@ -45,9 +47,7 @@ def _get_env_llm_conf(llm_type: str) -> Dict[str, Any]:
|
|||||||
return conf
|
return conf
|
||||||
|
|
||||||
|
|
||||||
def _create_llm_use_conf(
|
def _create_llm_use_conf(llm_type: LLMType, conf: Dict[str, Any]) -> BaseChatModel:
|
||||||
llm_type: LLMType, conf: Dict[str, Any]
|
|
||||||
) -> ChatOpenAI | ChatDeepSeek:
|
|
||||||
"""Create LLM instance using configuration."""
|
"""Create LLM instance using configuration."""
|
||||||
llm_type_config_keys = _get_llm_type_config_keys()
|
llm_type_config_keys = _get_llm_type_config_keys()
|
||||||
config_key = llm_type_config_keys.get(llm_type)
|
config_key = llm_type_config_keys.get(llm_type)
|
||||||
@@ -68,19 +68,34 @@ def _create_llm_use_conf(
|
|||||||
if not merged_conf:
|
if not merged_conf:
|
||||||
raise ValueError(f"No configuration found for LLM type: {llm_type}")
|
raise ValueError(f"No configuration found for LLM type: {llm_type}")
|
||||||
|
|
||||||
|
# Add max_retries to handle rate limit errors
|
||||||
|
if "max_retries" not in merged_conf:
|
||||||
|
merged_conf["max_retries"] = 3
|
||||||
|
|
||||||
if llm_type == "reasoning":
|
if llm_type == "reasoning":
|
||||||
merged_conf["api_base"] = merged_conf.pop("base_url", None)
|
merged_conf["api_base"] = merged_conf.pop("base_url", None)
|
||||||
|
|
||||||
return (
|
# Handle SSL verification settings
|
||||||
ChatOpenAI(**merged_conf)
|
verify_ssl = merged_conf.pop("verify_ssl", True)
|
||||||
if llm_type != "reasoning"
|
|
||||||
else ChatDeepSeek(**merged_conf)
|
# Create custom HTTP client if SSL verification is disabled
|
||||||
)
|
if not verify_ssl:
|
||||||
|
http_client = httpx.Client(verify=False)
|
||||||
|
http_async_client = httpx.AsyncClient(verify=False)
|
||||||
|
merged_conf["http_client"] = http_client
|
||||||
|
merged_conf["http_async_client"] = http_async_client
|
||||||
|
|
||||||
|
if "azure_endpoint" in merged_conf or os.getenv("AZURE_OPENAI_ENDPOINT"):
|
||||||
|
return AzureChatOpenAI(**merged_conf)
|
||||||
|
if llm_type == "reasoning":
|
||||||
|
return ChatDeepSeek(**merged_conf)
|
||||||
|
else:
|
||||||
|
return ChatOpenAI(**merged_conf)
|
||||||
|
|
||||||
|
|
||||||
def get_llm_by_type(
|
def get_llm_by_type(
|
||||||
llm_type: LLMType,
|
llm_type: LLMType,
|
||||||
) -> ChatOpenAI:
|
) -> BaseChatModel:
|
||||||
"""
|
"""
|
||||||
Get LLM instance by type. Returns cached instance if available.
|
Get LLM instance by type. Returns cached instance if available.
|
||||||
"""
|
"""
|
||||||
@@ -133,9 +148,3 @@ def get_configured_llm_models() -> dict[str, list[str]]:
|
|||||||
# In the future, we will use reasoning_llm and vl_llm for different purposes
|
# In the future, we will use reasoning_llm and vl_llm for different purposes
|
||||||
# reasoning_llm = get_llm_by_type("reasoning")
|
# reasoning_llm = get_llm_by_type("reasoning")
|
||||||
# vl_llm = get_llm_by_type("vision")
|
# vl_llm = get_llm_by_type("vision")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Initialize LLMs for different purposes - now these will be cached
|
|
||||||
basic_llm = get_llm_by_type("basic")
|
|
||||||
print(basic_llm.invoke("Hello"))
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from langgraph.graph import MessagesState
|
from langgraph.graph import MessagesState
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
from langchain.schema import HumanMessage, SystemMessage
|
from langchain.schema import HumanMessage
|
||||||
|
|
||||||
from src.config.agents import AGENT_LLM_MAP
|
from src.config.agents import AGENT_LLM_MAP
|
||||||
from src.llms.llm import get_llm_by_type
|
from src.llms.llm import get_llm_by_type
|
||||||
from src.prompts.template import env, apply_prompt_template
|
from src.prompts.template import apply_prompt_template
|
||||||
from src.prompt_enhancer.graph.state import PromptEnhancerState
|
from src.prompt_enhancer.graph.state import PromptEnhancerState
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -41,23 +42,38 @@ def prompt_enhancer_node(state: PromptEnhancerState):
|
|||||||
# Get the response from the model
|
# Get the response from the model
|
||||||
response = model.invoke(messages)
|
response = model.invoke(messages)
|
||||||
|
|
||||||
# Clean up the response - remove any extra formatting or comments
|
# Extract content from response
|
||||||
enhanced_prompt = response.content.strip()
|
response_content = response.content.strip()
|
||||||
|
logger.debug(f"Response content: {response_content}")
|
||||||
|
|
||||||
# Remove common prefixes that might be added by the model
|
# Try to extract content from XML tags first
|
||||||
prefixes_to_remove = [
|
xml_match = re.search(
|
||||||
"Enhanced Prompt:",
|
r"<enhanced_prompt>(.*?)</enhanced_prompt>", response_content, re.DOTALL
|
||||||
"Enhanced prompt:",
|
)
|
||||||
"Here's the enhanced prompt:",
|
|
||||||
"Here is the enhanced prompt:",
|
|
||||||
"**Enhanced Prompt**:",
|
|
||||||
"**Enhanced prompt**:",
|
|
||||||
]
|
|
||||||
|
|
||||||
for prefix in prefixes_to_remove:
|
if xml_match:
|
||||||
if enhanced_prompt.startswith(prefix):
|
# Extract content from XML tags and clean it up
|
||||||
enhanced_prompt = enhanced_prompt[len(prefix) :].strip()
|
enhanced_prompt = xml_match.group(1).strip()
|
||||||
break
|
logger.debug("Successfully extracted enhanced prompt from XML tags")
|
||||||
|
else:
|
||||||
|
# Fallback to original logic if no XML tags found
|
||||||
|
enhanced_prompt = response_content
|
||||||
|
logger.warning("No XML tags found in response, using fallback parsing")
|
||||||
|
|
||||||
|
# Remove common prefixes that might be added by the model
|
||||||
|
prefixes_to_remove = [
|
||||||
|
"Enhanced Prompt:",
|
||||||
|
"Enhanced prompt:",
|
||||||
|
"Here's the enhanced prompt:",
|
||||||
|
"Here is the enhanced prompt:",
|
||||||
|
"**Enhanced Prompt**:",
|
||||||
|
"**Enhanced prompt**:",
|
||||||
|
]
|
||||||
|
|
||||||
|
for prefix in prefixes_to_remove:
|
||||||
|
if enhanced_prompt.startswith(prefix):
|
||||||
|
enhanced_prompt = enhanced_prompt[len(prefix) :].strip()
|
||||||
|
break
|
||||||
|
|
||||||
logger.info("Prompt enhancement completed successfully")
|
logger.info("Prompt enhancement completed successfully")
|
||||||
logger.debug(f"Enhanced prompt: {enhanced_prompt}")
|
logger.debug(f"Enhanced prompt: {enhanced_prompt}")
|
||||||
|
|||||||
@@ -52,53 +52,84 @@ You are an expert prompt engineer. Your task is to enhance user prompts to make
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
# Output Requirements
|
# Output Requirements
|
||||||
- Output ONLY the enhanced prompt
|
- You may include thoughts or reasoning before your final answer
|
||||||
- Do NOT include any explanations, comments, or meta-text
|
- Wrap the final enhanced prompt in XML tags: <enhanced_prompt></enhanced_prompt>
|
||||||
- Do NOT use phrases like "Enhanced Prompt:" or "Here's the enhanced version:"
|
- Do NOT include any explanations, comments, or meta-text within the XML tags
|
||||||
- The output should be ready to use directly as a prompt
|
- Do NOT use phrases like "Enhanced Prompt:" or "Here's the enhanced version:" within the XML tags
|
||||||
|
- The content within the XML tags should be ready to use directly as a prompt
|
||||||
|
|
||||||
{% if report_style == "academic" %}
|
{% if report_style == "academic" %}
|
||||||
# Academic Style Examples
|
# Academic Style Examples
|
||||||
|
|
||||||
**Original**: "Write about AI"
|
**Original**: "Write about AI"
|
||||||
**Enhanced**: "Conduct a comprehensive academic analysis of artificial intelligence applications across three key sectors: healthcare, education, and business. Employ a systematic literature review methodology to examine peer-reviewed sources from the past five years. Structure your analysis with: (1) theoretical framework defining AI and its taxonomies, (2) sector-specific case studies with quantitative performance metrics, (3) critical evaluation of implementation challenges and ethical considerations, (4) comparative analysis across sectors, and (5) evidence-based recommendations for future research directions. Maintain academic rigor with proper citations, acknowledge methodological limitations, and present findings with appropriate hedging language. Target length: 3000-4000 words with APA formatting."
|
**Enhanced**:
|
||||||
|
<enhanced_prompt>
|
||||||
|
Conduct a comprehensive academic analysis of artificial intelligence applications across three key sectors: healthcare, education, and business. Employ a systematic literature review methodology to examine peer-reviewed sources from the past five years. Structure your analysis with: (1) theoretical framework defining AI and its taxonomies, (2) sector-specific case studies with quantitative performance metrics, (3) critical evaluation of implementation challenges and ethical considerations, (4) comparative analysis across sectors, and (5) evidence-based recommendations for future research directions. Maintain academic rigor with proper citations, acknowledge methodological limitations, and present findings with appropriate hedging language. Target length: 3000-4000 words with APA formatting.
|
||||||
|
</enhanced_prompt>
|
||||||
|
|
||||||
**Original**: "Explain climate change"
|
**Original**: "Explain climate change"
|
||||||
**Enhanced**: "Provide a rigorous academic examination of anthropogenic climate change, synthesizing current scientific consensus and recent research developments. Structure your analysis as follows: (1) theoretical foundations of greenhouse effect and radiative forcing mechanisms, (2) systematic review of empirical evidence from paleoclimatic, observational, and modeling studies, (3) critical analysis of attribution studies linking human activities to observed warming, (4) evaluation of climate sensitivity estimates and uncertainty ranges, (5) assessment of projected impacts under different emission scenarios, and (6) discussion of research gaps and methodological limitations. Include quantitative data, statistical significance levels, and confidence intervals where appropriate. Cite peer-reviewed sources extensively and maintain objective, third-person academic voice throughout."
|
**Enhanced**:
|
||||||
|
<enhanced_prompt>
|
||||||
|
Provide a rigorous academic examination of anthropogenic climate change, synthesizing current scientific consensus and recent research developments. Structure your analysis as follows: (1) theoretical foundations of greenhouse effect and radiative forcing mechanisms, (2) systematic review of empirical evidence from paleoclimatic, observational, and modeling studies, (3) critical analysis of attribution studies linking human activities to observed warming, (4) evaluation of climate sensitivity estimates and uncertainty ranges, (5) assessment of projected impacts under different emission scenarios, and (6) discussion of research gaps and methodological limitations. Include quantitative data, statistical significance levels, and confidence intervals where appropriate. Cite peer-reviewed sources extensively and maintain objective, third-person academic voice throughout.
|
||||||
|
</enhanced_prompt>
|
||||||
|
|
||||||
{% elif report_style == "popular_science" %}
|
{% elif report_style == "popular_science" %}
|
||||||
# Popular Science Style Examples
|
# Popular Science Style Examples
|
||||||
|
|
||||||
**Original**: "Write about AI"
|
**Original**: "Write about AI"
|
||||||
**Enhanced**: "Tell the fascinating story of how artificial intelligence is quietly revolutionizing our daily lives in ways most people never realize. Take readers on an engaging journey through three surprising realms: the hospital where AI helps doctors spot diseases faster than ever before, the classroom where intelligent tutors adapt to each student's learning style, and the boardroom where algorithms are making million-dollar decisions. Use vivid analogies (like comparing neural networks to how our brains work) and real-world examples that readers can relate to. Include 'wow factor' moments that showcase AI's incredible capabilities, but also honest discussions about current limitations. Write with infectious enthusiasm while maintaining scientific accuracy, and conclude with exciting possibilities that await us in the near future. Aim for 1500-2000 words that feel like a captivating conversation with a brilliant friend."
|
**Enhanced**:
|
||||||
|
<enhanced_prompt>
|
||||||
|
Tell the fascinating story of how artificial intelligence is quietly revolutionizing our daily lives in ways most people never realize. Take readers on an engaging journey through three surprising realms: the hospital where AI helps doctors spot diseases faster than ever before, the classroom where intelligent tutors adapt to each student's learning style, and the boardroom where algorithms are making million-dollar decisions. Use vivid analogies (like comparing neural networks to how our brains work) and real-world examples that readers can relate to. Include 'wow factor' moments that showcase AI's incredible capabilities, but also honest discussions about current limitations. Write with infectious enthusiasm while maintaining scientific accuracy, and conclude with exciting possibilities that await us in the near future. Aim for 1500-2000 words that feel like a captivating conversation with a brilliant friend.
|
||||||
|
</enhanced_prompt>
|
||||||
|
|
||||||
**Original**: "Explain climate change"
|
**Original**: "Explain climate change"
|
||||||
**Enhanced**: "Craft a compelling narrative that transforms the complex science of climate change into an accessible and engaging story for curious readers. Begin with a relatable scenario (like why your hometown weather feels different than when you were a kid) and use this as a gateway to explore the fascinating science behind our changing planet. Employ vivid analogies - compare Earth's atmosphere to a blanket, greenhouse gases to invisible heat-trapping molecules, and climate feedback loops to a snowball rolling downhill. Include surprising facts and 'aha moments' that will make readers think differently about the world around them. Weave in human stories of scientists making discoveries, communities adapting to change, and innovative solutions being developed. Balance the serious implications with hope and actionable insights, concluding with empowering steps readers can take. Write with wonder and curiosity, making complex concepts feel approachable and personally relevant."
|
**Enhanced**:
|
||||||
|
<enhanced_prompt>
|
||||||
|
Craft a compelling narrative that transforms the complex science of climate change into an accessible and engaging story for curious readers. Begin with a relatable scenario (like why your hometown weather feels different than when you were a kid) and use this as a gateway to explore the fascinating science behind our changing planet. Employ vivid analogies - compare Earth's atmosphere to a blanket, greenhouse gases to invisible heat-trapping molecules, and climate feedback loops to a snowball rolling downhill. Include surprising facts and 'aha moments' that will make readers think differently about the world around them. Weave in human stories of scientists making discoveries, communities adapting to change, and innovative solutions being developed. Balance the serious implications with hope and actionable insights, concluding with empowering steps readers can take. Write with wonder and curiosity, making complex concepts feel approachable and personally relevant.
|
||||||
|
</enhanced_prompt>
|
||||||
|
|
||||||
{% elif report_style == "news" %}
|
{% elif report_style == "news" %}
|
||||||
# News Style Examples
|
# News Style Examples
|
||||||
|
|
||||||
**Original**: "Write about AI"
|
**Original**: "Write about AI"
|
||||||
**Enhanced**: "Report on the current state and immediate impact of artificial intelligence across three critical sectors: healthcare, education, and business. Lead with the most newsworthy developments and recent breakthroughs that are affecting people today. Structure using inverted pyramid format: start with key findings and immediate implications, then provide essential background context, followed by detailed analysis and expert perspectives. Include specific, verifiable data points, recent statistics, and quotes from credible sources including industry leaders, researchers, and affected stakeholders. Address both benefits and concerns with balanced reporting, fact-check all claims, and provide proper attribution for all information. Focus on timeliness and relevance to current events, highlighting what's happening now and what readers need to know. Maintain journalistic objectivity while making the significance clear to a general news audience. Target 800-1200 words following AP style guidelines."
|
**Enhanced**:
|
||||||
|
<enhanced_prompt>
|
||||||
|
Report on the current state and immediate impact of artificial intelligence across three critical sectors: healthcare, education, and business. Lead with the most newsworthy developments and recent breakthroughs that are affecting people today. Structure using inverted pyramid format: start with key findings and immediate implications, then provide essential background context, followed by detailed analysis and expert perspectives. Include specific, verifiable data points, recent statistics, and quotes from credible sources including industry leaders, researchers, and affected stakeholders. Address both benefits and concerns with balanced reporting, fact-check all claims, and provide proper attribution for all information. Focus on timeliness and relevance to current events, highlighting what's happening now and what readers need to know. Maintain journalistic objectivity while making the significance clear to a general news audience. Target 800-1200 words following AP style guidelines.
|
||||||
|
</enhanced_prompt>
|
||||||
|
|
||||||
**Original**: "Explain climate change"
|
**Original**: "Explain climate change"
|
||||||
**Enhanced**: "Provide comprehensive news coverage of climate change that explains the current scientific understanding and immediate implications for readers. Lead with the most recent and significant developments in climate science, policy, or impacts that are making headlines today. Structure the report with: breaking developments first, essential background for understanding the issue, current scientific consensus with specific data and timeframes, real-world impacts already being observed, policy responses and debates, and what experts say comes next. Include quotes from credible climate scientists, policy makers, and affected communities. Present information objectively while clearly communicating the scientific consensus, fact-check all claims, and provide proper source attribution. Address common misconceptions with factual corrections. Focus on what's happening now, why it matters to readers, and what they can expect in the near future. Follow journalistic standards for accuracy, balance, and timeliness."
|
**Enhanced**:
|
||||||
|
<enhanced_prompt>
|
||||||
|
Provide comprehensive news coverage of climate change that explains the current scientific understanding and immediate implications for readers. Lead with the most recent and significant developments in climate science, policy, or impacts that are making headlines today. Structure the report with: breaking developments first, essential background for understanding the issue, current scientific consensus with specific data and timeframes, real-world impacts already being observed, policy responses and debates, and what experts say comes next. Include quotes from credible climate scientists, policy makers, and affected communities. Present information objectively while clearly communicating the scientific consensus, fact-check all claims, and provide proper source attribution. Address common misconceptions with factual corrections. Focus on what's happening now, why it matters to readers, and what they can expect in the near future. Follow journalistic standards for accuracy, balance, and timeliness.
|
||||||
|
</enhanced_prompt>
|
||||||
|
|
||||||
{% elif report_style == "social_media" %}
|
{% elif report_style == "social_media" %}
|
||||||
# Social Media Style Examples
|
# Social Media Style Examples
|
||||||
|
|
||||||
**Original**: "Write about AI"
|
**Original**: "Write about AI"
|
||||||
**Enhanced**: "Create engaging social media content about AI that will stop the scroll and spark conversations! Start with an attention-grabbing hook like 'You won't believe what AI just did in hospitals this week 🤯' and structure as a compelling thread or post series. Include surprising facts, relatable examples (like AI helping doctors spot diseases or personalizing your Netflix recommendations), and interactive elements that encourage sharing and comments. Use strategic hashtags (#AI #Technology #Future), incorporate relevant emojis for visual appeal, and include questions that prompt audience engagement ('Have you noticed AI in your daily life? Drop examples below! 👇'). Make complex concepts digestible with bite-sized explanations, trending analogies, and shareable quotes. Include a clear call-to-action and optimize for the specific platform (Twitter threads, Instagram carousel, LinkedIn professional insights, or TikTok-style quick facts). Aim for high shareability with content that feels both informative and entertaining."
|
**Enhanced**:
|
||||||
|
<enhanced_prompt>
|
||||||
|
Create engaging social media content about AI that will stop the scroll and spark conversations! Start with an attention-grabbing hook like 'You won't believe what AI just did in hospitals this week 🤯' and structure as a compelling thread or post series. Include surprising facts, relatable examples (like AI helping doctors spot diseases or personalizing your Netflix recommendations), and interactive elements that encourage sharing and comments. Use strategic hashtags (#AI #Technology #Future), incorporate relevant emojis for visual appeal, and include questions that prompt audience engagement ('Have you noticed AI in your daily life? Drop examples below! 👇'). Make complex concepts digestible with bite-sized explanations, trending analogies, and shareable quotes. Include a clear call-to-action and optimize for the specific platform (Twitter threads, Instagram carousel, LinkedIn professional insights, or TikTok-style quick facts). Aim for high shareability with content that feels both informative and entertaining.
|
||||||
|
</enhanced_prompt>
|
||||||
|
|
||||||
**Original**: "Explain climate change"
|
**Original**: "Explain climate change"
|
||||||
**Enhanced**: "Develop viral-worthy social media content that makes climate change accessible and shareable without being preachy. Open with a scroll-stopping hook like 'The weather app on your phone is telling a bigger story than you think 📱🌡️' and break down complex science into digestible, engaging chunks. Use relatable comparisons (Earth's fever, atmosphere as a blanket), trending formats (before/after visuals, myth-busting series, quick facts), and interactive elements (polls, questions, challenges). Include strategic hashtags (#ClimateChange #Science #Environment), eye-catching emojis, and shareable graphics or infographics. Address common questions and misconceptions with clear, factual responses. Create content that encourages positive action rather than climate anxiety, ending with empowering steps followers can take. Optimize for platform-specific features (Instagram Stories, TikTok trends, Twitter threads) and include calls-to-action that drive engagement and sharing."
|
**Enhanced**:
|
||||||
|
<enhanced_prompt>
|
||||||
|
Develop viral-worthy social media content that makes climate change accessible and shareable without being preachy. Open with a scroll-stopping hook like 'The weather app on your phone is telling a bigger story than you think 📱🌡️' and break down complex science into digestible, engaging chunks. Use relatable comparisons (Earth's fever, atmosphere as a blanket), trending formats (before/after visuals, myth-busting series, quick facts), and interactive elements (polls, questions, challenges). Include strategic hashtags (#ClimateChange #Science #Environment), eye-catching emojis, and shareable graphics or infographics. Address common questions and misconceptions with clear, factual responses. Create content that encourages positive action rather than climate anxiety, ending with empowering steps followers can take. Optimize for platform-specific features (Instagram Stories, TikTok trends, Twitter threads) and include calls-to-action that drive engagement and sharing.
|
||||||
|
</enhanced_prompt>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
# General Examples
|
# General Examples
|
||||||
|
|
||||||
**Original**: "Write about AI"
|
**Original**: "Write about AI"
|
||||||
**Enhanced**: "Write a comprehensive 1000-word analysis of artificial intelligence's current applications in healthcare, education, and business. Include specific examples of AI tools being used in each sector, discuss both benefits and challenges, and provide insights into future trends. Structure the response with clear sections for each industry and conclude with key takeaways."
|
**Enhanced**:
|
||||||
|
<enhanced_prompt>
|
||||||
|
Write a comprehensive 1000-word analysis of artificial intelligence's current applications in healthcare, education, and business. Include specific examples of AI tools being used in each sector, discuss both benefits and challenges, and provide insights into future trends. Structure the response with clear sections for each industry and conclude with key takeaways.
|
||||||
|
</enhanced_prompt>
|
||||||
|
|
||||||
**Original**: "Explain climate change"
|
**Original**: "Explain climate change"
|
||||||
**Enhanced**: "Provide a detailed explanation of climate change suitable for a general audience. Cover the scientific mechanisms behind global warming, major causes including greenhouse gas emissions, observable effects we're seeing today, and projected future impacts. Include specific data and examples, and explain the difference between weather and climate. Organize the response with clear headings and conclude with actionable steps individuals can take."
|
**Enhanced**:
|
||||||
|
<enhanced_prompt>
|
||||||
|
Provide a detailed explanation of climate change suitable for a general audience. Cover the scientific mechanisms behind global warming, major causes including greenhouse gas emissions, observable effects we're seeing today, and projected future impacts. Include specific data and examples, and explain the difference between weather and climate. Organize the response with clear headings and conclude with actionable steps individuals can take.
|
||||||
|
</enhanced_prompt>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
+11
-2
@@ -1,8 +1,17 @@
|
|||||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
from .retriever import Retriever, Document, Resource
|
from .retriever import Retriever, Document, Resource, Chunk
|
||||||
from .ragflow import RAGFlowProvider
|
from .ragflow import RAGFlowProvider
|
||||||
|
from .vikingdb_knowledge_base import VikingDBKnowledgeBaseProvider
|
||||||
from .builder import build_retriever
|
from .builder import build_retriever
|
||||||
|
|
||||||
__all__ = [Retriever, Document, Resource, RAGFlowProvider, build_retriever]
|
__all__ = [
|
||||||
|
Retriever,
|
||||||
|
Document,
|
||||||
|
Resource,
|
||||||
|
RAGFlowProvider,
|
||||||
|
VikingDBKnowledgeBaseProvider,
|
||||||
|
Chunk,
|
||||||
|
build_retriever,
|
||||||
|
]
|
||||||
|
|||||||
@@ -3,12 +3,15 @@
|
|||||||
|
|
||||||
from src.config.tools import SELECTED_RAG_PROVIDER, RAGProvider
|
from src.config.tools import SELECTED_RAG_PROVIDER, RAGProvider
|
||||||
from src.rag.ragflow import RAGFlowProvider
|
from src.rag.ragflow import RAGFlowProvider
|
||||||
|
from src.rag.vikingdb_knowledge_base import VikingDBKnowledgeBaseProvider
|
||||||
from src.rag.retriever import Retriever
|
from src.rag.retriever import Retriever
|
||||||
|
|
||||||
|
|
||||||
def build_retriever() -> Retriever | None:
|
def build_retriever() -> Retriever | None:
|
||||||
if SELECTED_RAG_PROVIDER == RAGProvider.RAGFLOW.value:
|
if SELECTED_RAG_PROVIDER == RAGProvider.RAGFLOW.value:
|
||||||
return RAGFlowProvider()
|
return RAGFlowProvider()
|
||||||
|
elif SELECTED_RAG_PROVIDER == RAGProvider.VIKINGDB_KNOWLEDGE_BASE.value:
|
||||||
|
return VikingDBKnowledgeBaseProvider()
|
||||||
elif SELECTED_RAG_PROVIDER:
|
elif SELECTED_RAG_PROVIDER:
|
||||||
raise ValueError(f"Unsupported RAG provider: {SELECTED_RAG_PROVIDER}")
|
raise ValueError(f"Unsupported RAG provider: {SELECTED_RAG_PROVIDER}")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -122,12 +122,3 @@ def parse_uri(uri: str) -> tuple[str, str]:
|
|||||||
if parsed.scheme != "rag":
|
if parsed.scheme != "rag":
|
||||||
raise ValueError(f"Invalid URI: {uri}")
|
raise ValueError(f"Invalid URI: {uri}")
|
||||||
return parsed.path.split("/")[1], parsed.fragment
|
return parsed.path.split("/")[1], parsed.fragment
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
uri = "rag://dataset/123#abc"
|
|
||||||
parsed = urlparse(uri)
|
|
||||||
print(parsed.scheme)
|
|
||||||
print(parsed.netloc)
|
|
||||||
print(parsed.path)
|
|
||||||
print(parsed.fragment)
|
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
from src.rag.retriever import Chunk, Document, Resource, Retriever
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from volcengine.auth.SignerV4 import SignerV4
|
||||||
|
from volcengine.base.Request import Request
|
||||||
|
from volcengine.Credentials import Credentials
|
||||||
|
|
||||||
|
|
||||||
|
class VikingDBKnowledgeBaseProvider(Retriever):
|
||||||
|
"""
|
||||||
|
VikingDBKnowledgeBaseProvider is a provider that uses VikingDB Knowledge base API to retrieve documents.
|
||||||
|
"""
|
||||||
|
|
||||||
|
api_url: str
|
||||||
|
api_ak: str
|
||||||
|
api_sk: str
|
||||||
|
retrieval_size: int = 10
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
api_url = os.getenv("VIKINGDB_KNOWLEDGE_BASE_API_URL")
|
||||||
|
if not api_url:
|
||||||
|
raise ValueError("VIKINGDB_KNOWLEDGE_BASE_API_URL is not set")
|
||||||
|
self.api_url = api_url
|
||||||
|
|
||||||
|
api_ak = os.getenv("VIKINGDB_KNOWLEDGE_BASE_API_AK")
|
||||||
|
if not api_ak:
|
||||||
|
raise ValueError("VIKINGDB_KNOWLEDGE_BASE_API_AK is not set")
|
||||||
|
self.api_ak = api_ak
|
||||||
|
|
||||||
|
api_sk = os.getenv("VIKINGDB_KNOWLEDGE_BASE_API_SK")
|
||||||
|
if not api_sk:
|
||||||
|
raise ValueError("VIKINGDB_KNOWLEDGE_BASE_API_SK is not set")
|
||||||
|
self.api_sk = api_sk
|
||||||
|
|
||||||
|
retrieval_size = os.getenv("VIKINGDB_KNOWLEDGE_BASE_RETRIEVAL_SIZE")
|
||||||
|
if retrieval_size:
|
||||||
|
self.retrieval_size = int(retrieval_size)
|
||||||
|
|
||||||
|
def prepare_request(self, method, path, params=None, data=None, doseq=0):
|
||||||
|
"""
|
||||||
|
Prepare signed request using volcengine auth
|
||||||
|
"""
|
||||||
|
if params:
|
||||||
|
for key in params:
|
||||||
|
if (
|
||||||
|
type(params[key]) is int
|
||||||
|
or type(params[key]) is float
|
||||||
|
or type(params[key]) is bool
|
||||||
|
):
|
||||||
|
params[key] = str(params[key])
|
||||||
|
elif type(params[key]) is list:
|
||||||
|
if not doseq:
|
||||||
|
params[key] = ",".join(params[key])
|
||||||
|
|
||||||
|
r = Request()
|
||||||
|
r.set_shema("https")
|
||||||
|
r.set_method(method)
|
||||||
|
r.set_connection_timeout(10)
|
||||||
|
r.set_socket_timeout(10)
|
||||||
|
mheaders = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
r.set_headers(mheaders)
|
||||||
|
if params:
|
||||||
|
r.set_query(params)
|
||||||
|
r.set_path(path)
|
||||||
|
if data is not None:
|
||||||
|
r.set_body(json.dumps(data))
|
||||||
|
|
||||||
|
credentials = Credentials(self.api_ak, self.api_sk, "air", "cn-north-1")
|
||||||
|
SignerV4.sign(r, credentials)
|
||||||
|
return r
|
||||||
|
|
||||||
|
def query_relevant_documents(
|
||||||
|
self, query: str, resources: list[Resource] = []
|
||||||
|
) -> list[Document]:
|
||||||
|
"""
|
||||||
|
Query relevant documents from the knowledge base
|
||||||
|
"""
|
||||||
|
if not resources:
|
||||||
|
return []
|
||||||
|
|
||||||
|
all_documents = {}
|
||||||
|
for resource in resources:
|
||||||
|
resource_id, document_id = parse_uri(resource.uri)
|
||||||
|
request_params = {
|
||||||
|
"resource_id": resource_id,
|
||||||
|
"query": query,
|
||||||
|
"limit": self.retrieval_size,
|
||||||
|
"dense_weight": 0.5,
|
||||||
|
"pre_processing": {
|
||||||
|
"need_instruction": True,
|
||||||
|
"rewrite": False,
|
||||||
|
"return_token_usage": True,
|
||||||
|
},
|
||||||
|
"post_processing": {
|
||||||
|
"rerank_switch": True,
|
||||||
|
"chunk_diffusion_count": 0,
|
||||||
|
"chunk_group": True,
|
||||||
|
"get_attachment_link": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if document_id:
|
||||||
|
doc_filter = {"op": "must", "field": "doc_id", "conds": [document_id]}
|
||||||
|
query_param = {"doc_filter": doc_filter}
|
||||||
|
request_params["query_param"] = query_param
|
||||||
|
|
||||||
|
method = "POST"
|
||||||
|
path = "/api/knowledge/collection/search_knowledge"
|
||||||
|
info_req = self.prepare_request(
|
||||||
|
method=method, path=path, data=request_params
|
||||||
|
)
|
||||||
|
rsp = requests.request(
|
||||||
|
method=info_req.method,
|
||||||
|
url="http://{}{}".format(self.api_url, info_req.path),
|
||||||
|
headers=info_req.headers,
|
||||||
|
data=info_req.body,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = json.loads(rsp.text)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise ValueError(f"Failed to parse JSON response: {e}")
|
||||||
|
|
||||||
|
if response["code"] != 0:
|
||||||
|
raise ValueError(
|
||||||
|
f"Failed to query documents from resource: {response['message']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
rsp_data = response.get("data", {})
|
||||||
|
|
||||||
|
if "result_list" not in rsp_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
result_list = rsp_data["result_list"]
|
||||||
|
|
||||||
|
for item in result_list:
|
||||||
|
doc_info = item.get("doc_info", {})
|
||||||
|
doc_id = doc_info.get("doc_id")
|
||||||
|
|
||||||
|
if not doc_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if doc_id not in all_documents:
|
||||||
|
all_documents[doc_id] = Document(
|
||||||
|
id=doc_id, title=doc_info.get("doc_name"), chunks=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
chunk = Chunk(
|
||||||
|
content=item.get("content", ""), similarity=item.get("score", 0.0)
|
||||||
|
)
|
||||||
|
all_documents[doc_id].chunks.append(chunk)
|
||||||
|
|
||||||
|
return list(all_documents.values())
|
||||||
|
|
||||||
|
def list_resources(self, query: str | None = None) -> list[Resource]:
|
||||||
|
"""
|
||||||
|
List resources (knowledge bases) from the knowledge base service
|
||||||
|
"""
|
||||||
|
method = "POST"
|
||||||
|
path = "/api/knowledge/collection/list"
|
||||||
|
info_req = self.prepare_request(method=method, path=path)
|
||||||
|
rsp = requests.request(
|
||||||
|
method=info_req.method,
|
||||||
|
url="http://{}{}".format(self.api_url, info_req.path),
|
||||||
|
headers=info_req.headers,
|
||||||
|
data=info_req.body,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
response = json.loads(rsp.text)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise ValueError(f"Failed to parse JSON response: {e}")
|
||||||
|
|
||||||
|
if response["code"] != 0:
|
||||||
|
raise Exception(f"Failed to list resources: {response["message"]}")
|
||||||
|
|
||||||
|
resources = []
|
||||||
|
rsp_data = response.get("data", {})
|
||||||
|
collection_list = rsp_data.get("collection_list", [])
|
||||||
|
for item in collection_list:
|
||||||
|
collection_name = item.get("collection_name", "")
|
||||||
|
description = item.get("description", "")
|
||||||
|
|
||||||
|
if query and query.lower() not in collection_name.lower():
|
||||||
|
continue
|
||||||
|
|
||||||
|
resource_id = item.get("resource_id", "")
|
||||||
|
resource = Resource(
|
||||||
|
uri=f"rag://dataset/{resource_id}",
|
||||||
|
title=collection_name,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
resources.append(resource)
|
||||||
|
|
||||||
|
return resources
|
||||||
|
|
||||||
|
|
||||||
|
def parse_uri(uri: str) -> tuple[str, str]:
|
||||||
|
parsed = urlparse(uri)
|
||||||
|
if parsed.scheme != "rag":
|
||||||
|
raise ValueError(f"Invalid URI: {uri}")
|
||||||
|
return parsed.path.split("/")[1], parsed.fragment
|
||||||
+30
-27
@@ -11,16 +11,17 @@ from uuid import uuid4
|
|||||||
from fastapi import FastAPI, HTTPException, Query
|
from fastapi import FastAPI, HTTPException, Query
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import Response, StreamingResponse
|
from fastapi.responses import Response, StreamingResponse
|
||||||
from langchain_core.messages import AIMessageChunk, ToolMessage, BaseMessage
|
from langchain_core.messages import AIMessageChunk, BaseMessage, ToolMessage
|
||||||
from langgraph.types import Command
|
from langgraph.types import Command
|
||||||
|
|
||||||
from src.config.report_style import ReportStyle
|
from src.config.report_style import ReportStyle
|
||||||
from src.config.tools import SELECTED_RAG_PROVIDER
|
from src.config.tools import SELECTED_RAG_PROVIDER
|
||||||
from src.graph.builder import build_graph_with_memory
|
from src.graph.builder import build_graph_with_memory
|
||||||
|
from src.llms.llm import get_configured_llm_models
|
||||||
from src.podcast.graph.builder import build_graph as build_podcast_graph
|
from src.podcast.graph.builder import build_graph as build_podcast_graph
|
||||||
from src.ppt.graph.builder import build_graph as build_ppt_graph
|
from src.ppt.graph.builder import build_graph as build_ppt_graph
|
||||||
from src.prose.graph.builder import build_graph as build_prose_graph
|
|
||||||
from src.prompt_enhancer.graph.builder import build_graph as build_prompt_enhancer_graph
|
from src.prompt_enhancer.graph.builder import build_graph as build_prompt_enhancer_graph
|
||||||
|
from src.prose.graph.builder import build_graph as build_prose_graph
|
||||||
from src.rag.builder import build_retriever
|
from src.rag.builder import build_retriever
|
||||||
from src.rag.retriever import Resource
|
from src.rag.retriever import Resource
|
||||||
from src.server.chat_request import (
|
from src.server.chat_request import (
|
||||||
@@ -31,6 +32,7 @@ from src.server.chat_request import (
|
|||||||
GenerateProseRequest,
|
GenerateProseRequest,
|
||||||
TTSRequest,
|
TTSRequest,
|
||||||
)
|
)
|
||||||
|
from src.server.config_request import ConfigResponse
|
||||||
from src.server.mcp_request import MCPServerMetadataRequest, MCPServerMetadataResponse
|
from src.server.mcp_request import MCPServerMetadataRequest, MCPServerMetadataResponse
|
||||||
from src.server.mcp_utils import load_mcp_tools
|
from src.server.mcp_utils import load_mcp_tools
|
||||||
from src.server.rag_request import (
|
from src.server.rag_request import (
|
||||||
@@ -38,8 +40,6 @@ from src.server.rag_request import (
|
|||||||
RAGResourceRequest,
|
RAGResourceRequest,
|
||||||
RAGResourcesResponse,
|
RAGResourcesResponse,
|
||||||
)
|
)
|
||||||
from src.server.config_request import ConfigResponse
|
|
||||||
from src.llms.llm import get_configured_llm_models
|
|
||||||
from src.tools import VolcengineTTS
|
from src.tools import VolcengineTTS
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -53,12 +53,17 @@ app = FastAPI(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Add CORS middleware
|
# Add CORS middleware
|
||||||
|
# It's recommended to load the allowed origins from an environment variable
|
||||||
|
# for better security and flexibility across different environments.
|
||||||
|
allowed_origins_str = os.getenv("ALLOWED_ORIGINS", "http://localhost:3000")
|
||||||
|
allowed_origins = [origin.strip() for origin in allowed_origins_str.split(",")]
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"], # Allows all origins
|
allow_origins=allowed_origins, # Restrict to specific origins
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"], # Allows all methods
|
allow_methods=["GET", "POST"], # Be specific about allowed methods
|
||||||
allow_headers=["*"], # Allows all headers
|
allow_headers=["Content-Type", "Authorization", "X-Requested-With"], # Be specific
|
||||||
)
|
)
|
||||||
|
|
||||||
graph = build_graph_with_memory()
|
graph = build_graph_with_memory()
|
||||||
@@ -153,9 +158,13 @@ async def _astream_workflow_generator(
|
|||||||
message_chunk, message_metadata = cast(
|
message_chunk, message_metadata = cast(
|
||||||
tuple[BaseMessage, dict[str, any]], event_data
|
tuple[BaseMessage, dict[str, any]], event_data
|
||||||
)
|
)
|
||||||
|
# Handle empty agent tuple gracefully
|
||||||
|
agent_name = "unknown"
|
||||||
|
if agent and len(agent) > 0:
|
||||||
|
agent_name = agent[0].split(":")[0] if ":" in agent[0] else agent[0]
|
||||||
event_stream_message: dict[str, any] = {
|
event_stream_message: dict[str, any] = {
|
||||||
"thread_id": thread_id,
|
"thread_id": thread_id,
|
||||||
"agent": agent[0].split(":")[0],
|
"agent": agent_name,
|
||||||
"id": message_chunk.id,
|
"id": message_chunk.id,
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": message_chunk.content,
|
"content": message_chunk.content,
|
||||||
@@ -201,17 +210,16 @@ def _make_event(event_type: str, data: dict[str, any]):
|
|||||||
@app.post("/api/tts")
|
@app.post("/api/tts")
|
||||||
async def text_to_speech(request: TTSRequest):
|
async def text_to_speech(request: TTSRequest):
|
||||||
"""Convert text to speech using volcengine TTS API."""
|
"""Convert text to speech using volcengine TTS API."""
|
||||||
|
app_id = os.getenv("VOLCENGINE_TTS_APPID", "")
|
||||||
|
if not app_id:
|
||||||
|
raise HTTPException(status_code=400, detail="VOLCENGINE_TTS_APPID is not set")
|
||||||
|
access_token = os.getenv("VOLCENGINE_TTS_ACCESS_TOKEN", "")
|
||||||
|
if not access_token:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="VOLCENGINE_TTS_ACCESS_TOKEN is not set"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app_id = os.getenv("VOLCENGINE_TTS_APPID", "")
|
|
||||||
if not app_id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400, detail="VOLCENGINE_TTS_APPID is not set"
|
|
||||||
)
|
|
||||||
access_token = os.getenv("VOLCENGINE_TTS_ACCESS_TOKEN", "")
|
|
||||||
if not access_token:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400, detail="VOLCENGINE_TTS_ACCESS_TOKEN is not set"
|
|
||||||
)
|
|
||||||
cluster = os.getenv("VOLCENGINE_TTS_CLUSTER", "volcano_tts")
|
cluster = os.getenv("VOLCENGINE_TTS_CLUSTER", "volcano_tts")
|
||||||
voice_type = os.getenv("VOLCENGINE_TTS_VOICE_TYPE", "BV700_V2_streaming")
|
voice_type = os.getenv("VOLCENGINE_TTS_VOICE_TYPE", "BV700_V2_streaming")
|
||||||
|
|
||||||
@@ -249,6 +257,7 @@ async def text_to_speech(request: TTSRequest):
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Error in TTS endpoint: {str(e)}")
|
logger.exception(f"Error in TTS endpoint: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR_DETAIL)
|
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR_DETAIL)
|
||||||
@@ -327,13 +336,9 @@ async def enhance_prompt(request: EnhancePromptRequest):
|
|||||||
"POPULAR_SCIENCE": ReportStyle.POPULAR_SCIENCE,
|
"POPULAR_SCIENCE": ReportStyle.POPULAR_SCIENCE,
|
||||||
"NEWS": ReportStyle.NEWS,
|
"NEWS": ReportStyle.NEWS,
|
||||||
"SOCIAL_MEDIA": ReportStyle.SOCIAL_MEDIA,
|
"SOCIAL_MEDIA": ReportStyle.SOCIAL_MEDIA,
|
||||||
"academic": ReportStyle.ACADEMIC,
|
|
||||||
"popular_science": ReportStyle.POPULAR_SCIENCE,
|
|
||||||
"news": ReportStyle.NEWS,
|
|
||||||
"social_media": ReportStyle.SOCIAL_MEDIA,
|
|
||||||
}
|
}
|
||||||
report_style = style_mapping.get(
|
report_style = style_mapping.get(
|
||||||
request.report_style, ReportStyle.ACADEMIC
|
request.report_style.upper(), ReportStyle.ACADEMIC
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
# If invalid style, default to ACADEMIC
|
# If invalid style, default to ACADEMIC
|
||||||
@@ -388,10 +393,8 @@ async def mcp_server_metadata(request: MCPServerMetadataRequest):
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if not isinstance(e, HTTPException):
|
logger.exception(f"Error in MCP server metadata endpoint: {str(e)}")
|
||||||
logger.exception(f"Error in MCP server metadata endpoint: {str(e)}")
|
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR_DETAIL)
|
||||||
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR_DETAIL)
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/rag/config", response_model=RAGConfigResponse)
|
@app.get("/api/rag/config", response_model=RAGConfigResponse)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from mcp import ClientSession, StdioServerParameters
|
from mcp import ClientSession, StdioServerParameters
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from .crawl import crawl_tool
|
from .crawl import crawl_tool
|
||||||
from .python_repl import python_repl_tool
|
from .python_repl import python_repl_tool
|
||||||
from .retriever import get_retriever_tool
|
from .retriever import get_retriever_tool
|
||||||
|
|||||||
@@ -60,18 +60,3 @@ def get_retriever_tool(resources: List[Resource]) -> RetrieverTool | None:
|
|||||||
if not retriever:
|
if not retriever:
|
||||||
return None
|
return None
|
||||||
return RetrieverTool(retriever=retriever, resources=resources)
|
return RetrieverTool(retriever=retriever, resources=resources)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
resources = [
|
|
||||||
Resource(
|
|
||||||
uri="rag://dataset/1c7e2ea4362911f09a41c290d4b6a7f0",
|
|
||||||
title="西游记",
|
|
||||||
description="西游记是中国古代四大名著之一,讲述了唐僧师徒四人西天取经的故事。",
|
|
||||||
)
|
|
||||||
]
|
|
||||||
retriever_tool = get_retriever_tool(resources)
|
|
||||||
print(retriever_tool.name)
|
|
||||||
print(retriever_tool.description)
|
|
||||||
print(retriever_tool.args)
|
|
||||||
print(retriever_tool.invoke("三打白骨精"))
|
|
||||||
|
|||||||
+24
-13
@@ -1,15 +1,16 @@
|
|||||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
from langchain_community.tools import BraveSearch, DuckDuckGoSearchResults
|
from langchain_community.tools import BraveSearch, DuckDuckGoSearchResults
|
||||||
from langchain_community.tools.arxiv import ArxivQueryRun
|
from langchain_community.tools.arxiv import ArxivQueryRun
|
||||||
from langchain_community.utilities import ArxivAPIWrapper, BraveSearchWrapper
|
from langchain_community.utilities import ArxivAPIWrapper, BraveSearchWrapper
|
||||||
|
|
||||||
from src.config import SearchEngine, SELECTED_SEARCH_ENGINE
|
from src.config import SearchEngine, SELECTED_SEARCH_ENGINE
|
||||||
|
from src.config import load_yaml_config
|
||||||
from src.tools.tavily_search.tavily_search_results_with_images import (
|
from src.tools.tavily_search.tavily_search_results_with_images import (
|
||||||
TavilySearchResultsWithImages,
|
TavilySearchResultsWithImages,
|
||||||
)
|
)
|
||||||
@@ -25,18 +26,39 @@ LoggedBraveSearch = create_logged_tool(BraveSearch)
|
|||||||
LoggedArxivSearch = create_logged_tool(ArxivQueryRun)
|
LoggedArxivSearch = create_logged_tool(ArxivQueryRun)
|
||||||
|
|
||||||
|
|
||||||
|
def get_search_config():
|
||||||
|
config = load_yaml_config("conf.yaml")
|
||||||
|
search_config = config.get("SEARCH_ENGINE", {})
|
||||||
|
return search_config
|
||||||
|
|
||||||
|
|
||||||
# Get the selected search tool
|
# Get the selected search tool
|
||||||
def get_web_search_tool(max_search_results: int):
|
def get_web_search_tool(max_search_results: int):
|
||||||
|
search_config = get_search_config()
|
||||||
|
|
||||||
if SELECTED_SEARCH_ENGINE == SearchEngine.TAVILY.value:
|
if SELECTED_SEARCH_ENGINE == SearchEngine.TAVILY.value:
|
||||||
|
# Only get and apply include/exclude domains for Tavily
|
||||||
|
include_domains: Optional[List[str]] = search_config.get("include_domains", [])
|
||||||
|
exclude_domains: Optional[List[str]] = search_config.get("exclude_domains", [])
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Tavily search configuration loaded: include_domains={include_domains}, exclude_domains={exclude_domains}"
|
||||||
|
)
|
||||||
|
|
||||||
return LoggedTavilySearch(
|
return LoggedTavilySearch(
|
||||||
name="web_search",
|
name="web_search",
|
||||||
max_results=max_search_results,
|
max_results=max_search_results,
|
||||||
include_raw_content=True,
|
include_raw_content=True,
|
||||||
include_images=True,
|
include_images=True,
|
||||||
include_image_descriptions=True,
|
include_image_descriptions=True,
|
||||||
|
include_domains=include_domains,
|
||||||
|
exclude_domains=exclude_domains,
|
||||||
)
|
)
|
||||||
elif SELECTED_SEARCH_ENGINE == SearchEngine.DUCKDUCKGO.value:
|
elif SELECTED_SEARCH_ENGINE == SearchEngine.DUCKDUCKGO.value:
|
||||||
return LoggedDuckDuckGoSearch(name="web_search", max_results=max_search_results)
|
return LoggedDuckDuckGoSearch(
|
||||||
|
name="web_search",
|
||||||
|
num_results=max_search_results,
|
||||||
|
)
|
||||||
elif SELECTED_SEARCH_ENGINE == SearchEngine.BRAVE_SEARCH.value:
|
elif SELECTED_SEARCH_ENGINE == SearchEngine.BRAVE_SEARCH.value:
|
||||||
return LoggedBraveSearch(
|
return LoggedBraveSearch(
|
||||||
name="web_search",
|
name="web_search",
|
||||||
@@ -56,14 +78,3 @@ def get_web_search_tool(max_search_results: int):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported search engine: {SELECTED_SEARCH_ENGINE}")
|
raise ValueError(f"Unsupported search engine: {SELECTED_SEARCH_ENGINE}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
results = LoggedDuckDuckGoSearch(
|
|
||||||
name="web_search", max_results=3, output_format="list"
|
|
||||||
)
|
|
||||||
print(results.name)
|
|
||||||
print(results.description)
|
|
||||||
print(results.args)
|
|
||||||
# .invoke("cute panda")
|
|
||||||
# print(json.dumps(results, indent=2, ensure_ascii=False))
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
@@ -107,9 +111,3 @@ class EnhancedTavilySearchAPIWrapper(OriginalTavilySearchAPIWrapper):
|
|||||||
}
|
}
|
||||||
clean_results.append(clean_result)
|
clean_results.append(clean_result)
|
||||||
return clean_results
|
return clean_results
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
wrapper = EnhancedTavilySearchAPIWrapper()
|
|
||||||
results = wrapper.raw_results("cute panda", include_images=True)
|
|
||||||
print(json.dumps(results, indent=2, ensure_ascii=False))
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Dict, List, Optional, Tuple, Union
|
from typing import Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -129,4 +129,4 @@ class VolcengineTTS:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Error in TTS API call: {str(e)}")
|
logger.exception(f"Error in TTS API call: {str(e)}")
|
||||||
return {"success": False, "error": str(e), "audio_data": None}
|
return {"success": False, "error": "TTS API call error", "audio_data": None}
|
||||||
|
|||||||
+11
-15
@@ -19,21 +19,17 @@ def repair_json_output(content: str) -> str:
|
|||||||
str: Repaired JSON string, or original content if not JSON
|
str: Repaired JSON string, or original content if not JSON
|
||||||
"""
|
"""
|
||||||
content = content.strip()
|
content = content.strip()
|
||||||
if content.startswith(("{", "[")) or "```json" in content or "```ts" in content:
|
|
||||||
try:
|
|
||||||
# If content is wrapped in ```json code block, extract the JSON part
|
|
||||||
if content.startswith("```json"):
|
|
||||||
content = content.removeprefix("```json")
|
|
||||||
|
|
||||||
if content.startswith("```ts"):
|
try:
|
||||||
content = content.removeprefix("```ts")
|
# Try to repair and parse JSON
|
||||||
|
repaired_content = json_repair.loads(content)
|
||||||
|
if not isinstance(repaired_content, dict) and not isinstance(
|
||||||
|
repaired_content, list
|
||||||
|
):
|
||||||
|
logger.warning("Repaired content is not a valid JSON object or array.")
|
||||||
|
return content
|
||||||
|
content = json.dumps(repaired_content, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"JSON repair failed: {e}")
|
||||||
|
|
||||||
if content.endswith("```"):
|
|
||||||
content = content.removesuffix("```")
|
|
||||||
|
|
||||||
# Try to repair and parse JSON
|
|
||||||
repaired_content = json_repair.loads(content)
|
|
||||||
return json.dumps(repaired_content, ensure_ascii=False)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"JSON repair failed: {e}")
|
|
||||||
return content
|
return content
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
from src.graph import build_graph
|
from src.graph import build_graph
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import pytest
|
|
||||||
from src.crawler import Crawler
|
from src.crawler import Crawler
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1258
-1
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
|||||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import pytest
|
|
||||||
from src.tools.python_repl import python_repl_tool
|
from src.tools.python_repl import python_repl_tool
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -101,8 +101,6 @@ def test_current_time_format():
|
|||||||
messages = apply_prompt_template("coder", test_state)
|
messages = apply_prompt_template("coder", test_state)
|
||||||
system_content = messages[0]["content"]
|
system_content = messages[0]["content"]
|
||||||
|
|
||||||
# Time format should be like: Mon Jan 01 2024 12:34:56 +0000
|
|
||||||
time_format = r"\w{3} \w{3} \d{2} \d{4} \d{2}:\d{2}:\d{2}"
|
|
||||||
assert any(
|
assert any(
|
||||||
line.strip().startswith("CURRENT_TIME:") for line in system_content.split("\n")
|
line.strip().startswith("CURRENT_TIME:") for line in system_content.split("\n")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import pytest
|
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
import uuid
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from src.tools.tts import VolcengineTTS
|
from src.tools.tts import VolcengineTTS
|
||||||
@@ -229,3 +227,21 @@ class TestVolcengineTTS:
|
|||||||
args, kwargs = mock_post.call_args
|
args, kwargs = mock_post.call_args
|
||||||
request_json = json.loads(args[1])
|
request_json = json.loads(args[1])
|
||||||
assert request_json["user"]["uid"] == str(mock_uuid_value)
|
assert request_json["user"]["uid"] == str(mock_uuid_value)
|
||||||
|
|
||||||
|
@patch("src.tools.tts.requests.post")
|
||||||
|
def test_text_to_speech_request_exception(self, mock_post):
|
||||||
|
"""Test error handling when requests.post raises an exception."""
|
||||||
|
# Mock requests.post to raise an exception
|
||||||
|
mock_post.side_effect = Exception("Network error")
|
||||||
|
# Create TTS client
|
||||||
|
tts = VolcengineTTS(
|
||||||
|
appid="test_appid",
|
||||||
|
access_token="test_token",
|
||||||
|
)
|
||||||
|
# Call the method
|
||||||
|
result = tts.text_to_speech("Hello, world!")
|
||||||
|
# Verify the result
|
||||||
|
assert result["success"] is False
|
||||||
|
# The TTS error is caught and returned as a string
|
||||||
|
assert result["error"] == "TTS API call error"
|
||||||
|
assert result["audio_data"] is None
|
||||||
|
|||||||
+1
-2
@@ -1,10 +1,9 @@
|
|||||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import pytest
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from typing import Annotated, List, Optional
|
from typing import Annotated
|
||||||
|
|
||||||
# Import MessagesState directly from langgraph rather than through our application
|
# Import MessagesState directly from langgraph rather than through our application
|
||||||
from langgraph.graph import MessagesState
|
from langgraph.graph import MessagesState
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import os
|
|
||||||
import pytest
|
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
from pathlib import Path
|
|
||||||
import builtins
|
|
||||||
import importlib
|
|
||||||
from src.config.configuration import Configuration
|
from src.config.configuration import Configuration
|
||||||
|
|
||||||
# Patch sys.path so relative import works
|
# Patch sys.path so relative import works
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import yaml
|
|
||||||
import pytest
|
|
||||||
from src.config.loader import load_yaml_config, process_dict, replace_env_vars
|
from src.config.loader import load_yaml_config, process_dict, replace_env_vars
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
import pytest
|
|
||||||
from src.crawler.article import Article
|
from src.crawler.article import Article
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import pytest
|
|
||||||
import src.crawler as crawler_module
|
import src.crawler as crawler_module
|
||||||
from src.crawler import Crawler
|
|
||||||
|
|
||||||
|
|
||||||
def test_crawler_sets_article_url(monkeypatch):
|
def test_crawler_sets_article_url(monkeypatch):
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
import importlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import src.graph.builder as builder_mod
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_state():
|
||||||
|
class Step:
|
||||||
|
def __init__(self, execution_res=None, step_type=None):
|
||||||
|
self.execution_res = execution_res
|
||||||
|
self.step_type = step_type
|
||||||
|
|
||||||
|
class Plan:
|
||||||
|
def __init__(self, steps):
|
||||||
|
self.steps = steps
|
||||||
|
|
||||||
|
return {
|
||||||
|
"Step": Step,
|
||||||
|
"Plan": Plan,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_continue_to_running_research_team_no_plan(mock_state):
|
||||||
|
state = {"current_plan": None}
|
||||||
|
assert builder_mod.continue_to_running_research_team(state) == "planner"
|
||||||
|
|
||||||
|
|
||||||
|
def test_continue_to_running_research_team_no_steps(mock_state):
|
||||||
|
state = {"current_plan": mock_state["Plan"](steps=[])}
|
||||||
|
assert builder_mod.continue_to_running_research_team(state) == "planner"
|
||||||
|
|
||||||
|
|
||||||
|
def test_continue_to_running_research_team_all_executed(mock_state):
|
||||||
|
Step = mock_state["Step"]
|
||||||
|
Plan = mock_state["Plan"]
|
||||||
|
steps = [Step(execution_res=True), Step(execution_res=True)]
|
||||||
|
state = {"current_plan": Plan(steps=steps)}
|
||||||
|
assert builder_mod.continue_to_running_research_team(state) == "planner"
|
||||||
|
|
||||||
|
|
||||||
|
def test_continue_to_running_research_team_next_researcher(mock_state):
|
||||||
|
Step = mock_state["Step"]
|
||||||
|
Plan = mock_state["Plan"]
|
||||||
|
steps = [
|
||||||
|
Step(execution_res=True),
|
||||||
|
Step(execution_res=None, step_type=builder_mod.StepType.RESEARCH),
|
||||||
|
]
|
||||||
|
state = {"current_plan": Plan(steps=steps)}
|
||||||
|
assert builder_mod.continue_to_running_research_team(state) == "researcher"
|
||||||
|
|
||||||
|
|
||||||
|
def test_continue_to_running_research_team_next_coder(mock_state):
|
||||||
|
Step = mock_state["Step"]
|
||||||
|
Plan = mock_state["Plan"]
|
||||||
|
steps = [
|
||||||
|
Step(execution_res=True),
|
||||||
|
Step(execution_res=None, step_type=builder_mod.StepType.PROCESSING),
|
||||||
|
]
|
||||||
|
state = {"current_plan": Plan(steps=steps)}
|
||||||
|
assert builder_mod.continue_to_running_research_team(state) == "coder"
|
||||||
|
|
||||||
|
|
||||||
|
def test_continue_to_running_research_team_next_coder_withresult(mock_state):
|
||||||
|
Step = mock_state["Step"]
|
||||||
|
Plan = mock_state["Plan"]
|
||||||
|
steps = [
|
||||||
|
Step(execution_res=True),
|
||||||
|
Step(execution_res=True, step_type=builder_mod.StepType.PROCESSING),
|
||||||
|
]
|
||||||
|
state = {"current_plan": Plan(steps=steps)}
|
||||||
|
assert builder_mod.continue_to_running_research_team(state) == "planner"
|
||||||
|
|
||||||
|
|
||||||
|
def test_continue_to_running_research_team_default_planner(mock_state):
|
||||||
|
Step = mock_state["Step"]
|
||||||
|
Plan = mock_state["Plan"]
|
||||||
|
steps = [Step(execution_res=True), Step(execution_res=None, step_type=None)]
|
||||||
|
state = {"current_plan": Plan(steps=steps)}
|
||||||
|
assert builder_mod.continue_to_running_research_team(state) == "planner"
|
||||||
|
|
||||||
|
|
||||||
|
@patch("src.graph.builder.StateGraph")
|
||||||
|
def test_build_base_graph_adds_nodes_and_edges(MockStateGraph):
|
||||||
|
mock_builder = MagicMock()
|
||||||
|
MockStateGraph.return_value = mock_builder
|
||||||
|
|
||||||
|
builder_mod._build_base_graph()
|
||||||
|
|
||||||
|
# Check that all nodes and edges are added
|
||||||
|
assert mock_builder.add_edge.call_count >= 2
|
||||||
|
assert mock_builder.add_node.call_count >= 8
|
||||||
|
mock_builder.add_conditional_edges.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@patch("src.graph.builder._build_base_graph")
|
||||||
|
@patch("src.graph.builder.MemorySaver")
|
||||||
|
def test_build_graph_with_memory_uses_memory(MockMemorySaver, mock_build_base_graph):
|
||||||
|
mock_builder = MagicMock()
|
||||||
|
mock_build_base_graph.return_value = mock_builder
|
||||||
|
mock_memory = MagicMock()
|
||||||
|
MockMemorySaver.return_value = mock_memory
|
||||||
|
|
||||||
|
builder_mod.build_graph_with_memory()
|
||||||
|
|
||||||
|
mock_builder.compile.assert_called_once_with(checkpointer=mock_memory)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("src.graph.builder._build_base_graph")
|
||||||
|
def test_build_graph_without_memory(mock_build_base_graph):
|
||||||
|
mock_builder = MagicMock()
|
||||||
|
mock_build_base_graph.return_value = mock_builder
|
||||||
|
|
||||||
|
builder_mod.build_graph()
|
||||||
|
|
||||||
|
mock_builder.compile.assert_called_once_with()
|
||||||
|
|
||||||
|
|
||||||
|
def test_graph_is_compiled():
|
||||||
|
# The graph object should be the result of build_graph()
|
||||||
|
with patch("src.graph.builder._build_base_graph") as mock_base:
|
||||||
|
mock_builder = MagicMock()
|
||||||
|
mock_base.return_value = mock_builder
|
||||||
|
mock_builder.compile.return_value = "compiled_graph"
|
||||||
|
# reload the module to re-run the graph assignment
|
||||||
|
importlib.reload(sys.modules["src.graph.builder"])
|
||||||
|
assert builder_mod.graph is not None
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from src.llms import llm
|
||||||
|
|
||||||
|
|
||||||
|
class DummyChatOpenAI:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
def invoke(self, msg):
|
||||||
|
return f"Echo: {msg}"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def patch_chat_openai(monkeypatch):
|
||||||
|
monkeypatch.setattr(llm, "ChatOpenAI", DummyChatOpenAI)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dummy_conf():
|
||||||
|
return {
|
||||||
|
"BASIC_MODEL": {"api_key": "test_key", "base_url": "http://test"},
|
||||||
|
"REASONING_MODEL": {"api_key": "reason_key"},
|
||||||
|
"VISION_MODEL": {"api_key": "vision_key"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_env_llm_conf(monkeypatch):
|
||||||
|
# Clear any existing environment variables that might interfere
|
||||||
|
monkeypatch.delenv("BASIC_MODEL__API_KEY", raising=False)
|
||||||
|
monkeypatch.delenv("BASIC_MODEL__BASE_URL", raising=False)
|
||||||
|
monkeypatch.delenv("BASIC_MODEL__MODEL", raising=False)
|
||||||
|
|
||||||
|
monkeypatch.setenv("BASIC_MODEL__API_KEY", "env_key")
|
||||||
|
monkeypatch.setenv("BASIC_MODEL__BASE_URL", "http://env")
|
||||||
|
conf = llm._get_env_llm_conf("basic")
|
||||||
|
assert conf["api_key"] == "env_key"
|
||||||
|
assert conf["base_url"] == "http://env"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_llm_use_conf_merges_env(monkeypatch, dummy_conf):
|
||||||
|
# Clear any existing environment variables that might interfere
|
||||||
|
monkeypatch.delenv("BASIC_MODEL__BASE_URL", raising=False)
|
||||||
|
monkeypatch.delenv("BASIC_MODEL__MODEL", raising=False)
|
||||||
|
monkeypatch.setenv("BASIC_MODEL__API_KEY", "env_key")
|
||||||
|
result = llm._create_llm_use_conf("basic", dummy_conf)
|
||||||
|
assert isinstance(result, DummyChatOpenAI)
|
||||||
|
assert result.kwargs["api_key"] == "env_key"
|
||||||
|
assert result.kwargs["base_url"] == "http://test"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_llm_use_conf_invalid_type(monkeypatch, dummy_conf):
|
||||||
|
# Clear any existing environment variables that might interfere
|
||||||
|
monkeypatch.delenv("BASIC_MODEL__API_KEY", raising=False)
|
||||||
|
monkeypatch.delenv("BASIC_MODEL__BASE_URL", raising=False)
|
||||||
|
monkeypatch.delenv("BASIC_MODEL__MODEL", raising=False)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
llm._create_llm_use_conf("unknown", dummy_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_llm_use_conf_empty_conf(monkeypatch):
|
||||||
|
# Clear any existing environment variables that might interfere
|
||||||
|
monkeypatch.delenv("BASIC_MODEL__API_KEY", raising=False)
|
||||||
|
monkeypatch.delenv("BASIC_MODEL__BASE_URL", raising=False)
|
||||||
|
monkeypatch.delenv("BASIC_MODEL__MODEL", raising=False)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
llm._create_llm_use_conf("basic", {})
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_llm_by_type_caches(monkeypatch, dummy_conf):
|
||||||
|
called = {}
|
||||||
|
|
||||||
|
def fake_load_yaml_config(path):
|
||||||
|
called["called"] = True
|
||||||
|
return dummy_conf
|
||||||
|
|
||||||
|
monkeypatch.setattr(llm, "load_yaml_config", fake_load_yaml_config)
|
||||||
|
llm._llm_cache.clear()
|
||||||
|
inst1 = llm.get_llm_by_type("basic")
|
||||||
|
inst2 = llm.get_llm_by_type("basic")
|
||||||
|
assert inst1 is inst2
|
||||||
|
assert called["called"]
|
||||||
@@ -6,7 +6,6 @@ from unittest.mock import patch, MagicMock
|
|||||||
|
|
||||||
from src.prompt_enhancer.graph.builder import build_graph
|
from src.prompt_enhancer.graph.builder import build_graph
|
||||||
from src.prompt_enhancer.graph.state import PromptEnhancerState
|
from src.prompt_enhancer.graph.state import PromptEnhancerState
|
||||||
from src.config.report_style import ReportStyle
|
|
||||||
|
|
||||||
|
|
||||||
class TestBuildGraph:
|
class TestBuildGraph:
|
||||||
@@ -48,7 +47,7 @@ class TestBuildGraph:
|
|||||||
mock_state_graph.return_value = mock_builder
|
mock_state_graph.return_value = mock_builder
|
||||||
mock_builder.compile.return_value = mock_compiled_graph
|
mock_builder.compile.return_value = mock_compiled_graph
|
||||||
|
|
||||||
result = build_graph()
|
build_graph()
|
||||||
|
|
||||||
# Verify the correct node function was added
|
# Verify the correct node function was added
|
||||||
mock_builder.add_node.assert_called_once_with("enhancer", mock_enhancer_node)
|
mock_builder.add_node.assert_called_once_with("enhancer", mock_enhancer_node)
|
||||||
|
|||||||
@@ -14,7 +14,75 @@ from src.config.report_style import ReportStyle
|
|||||||
def mock_llm():
|
def mock_llm():
|
||||||
"""Mock LLM that returns a test response."""
|
"""Mock LLM that returns a test response."""
|
||||||
llm = MagicMock()
|
llm = MagicMock()
|
||||||
llm.invoke.return_value = MagicMock(content="Enhanced test prompt")
|
llm.invoke.return_value = MagicMock(
|
||||||
|
content="""Thoughts: LLM thinks a lot
|
||||||
|
<enhanced_prompt>
|
||||||
|
Enhanced test prompt
|
||||||
|
</enhanced_prompt>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return llm
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_llm_xml_with_whitespace():
|
||||||
|
"""Mock LLM that returns XML response with extra whitespace."""
|
||||||
|
llm = MagicMock()
|
||||||
|
llm.invoke.return_value = MagicMock(
|
||||||
|
content="""
|
||||||
|
Some thoughts here...
|
||||||
|
|
||||||
|
<enhanced_prompt>
|
||||||
|
|
||||||
|
Enhanced prompt with whitespace
|
||||||
|
|
||||||
|
</enhanced_prompt>
|
||||||
|
|
||||||
|
Additional content after XML
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return llm
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_llm_xml_multiline():
|
||||||
|
"""Mock LLM that returns XML response with multiline content."""
|
||||||
|
llm = MagicMock()
|
||||||
|
llm.invoke.return_value = MagicMock(
|
||||||
|
content="""
|
||||||
|
<enhanced_prompt>
|
||||||
|
This is a multiline enhanced prompt
|
||||||
|
that spans multiple lines
|
||||||
|
and includes various formatting.
|
||||||
|
|
||||||
|
It should preserve the structure.
|
||||||
|
</enhanced_prompt>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return llm
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_llm_no_xml():
|
||||||
|
"""Mock LLM that returns response without XML tags."""
|
||||||
|
llm = MagicMock()
|
||||||
|
llm.invoke.return_value = MagicMock(
|
||||||
|
content="Enhanced Prompt: This is an enhanced prompt without XML tags"
|
||||||
|
)
|
||||||
|
return llm
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_llm_malformed_xml():
|
||||||
|
"""Mock LLM that returns response with malformed XML."""
|
||||||
|
llm = MagicMock()
|
||||||
|
llm.invoke.return_value = MagicMock(
|
||||||
|
content="""
|
||||||
|
<enhanced_prompt>
|
||||||
|
This XML tag is not properly closed
|
||||||
|
<enhanced_prompt>
|
||||||
|
"""
|
||||||
|
)
|
||||||
return llm
|
return llm
|
||||||
|
|
||||||
|
|
||||||
@@ -217,3 +285,241 @@ class TestPromptEnhancerNode:
|
|||||||
result = prompt_enhancer_node(state)
|
result = prompt_enhancer_node(state)
|
||||||
|
|
||||||
assert result == {"output": "Enhanced prompt"}
|
assert result == {"output": "Enhanced prompt"}
|
||||||
|
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.apply_prompt_template")
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.get_llm_by_type")
|
||||||
|
@patch(
|
||||||
|
"src.prompt_enhancer.graph.enhancer_node.AGENT_LLM_MAP",
|
||||||
|
{"prompt_enhancer": "basic"},
|
||||||
|
)
|
||||||
|
def test_xml_with_whitespace_handling(
|
||||||
|
self,
|
||||||
|
mock_get_llm,
|
||||||
|
mock_apply_template,
|
||||||
|
mock_llm_xml_with_whitespace,
|
||||||
|
mock_messages,
|
||||||
|
):
|
||||||
|
"""Test XML extraction with extra whitespace inside tags."""
|
||||||
|
mock_get_llm.return_value = mock_llm_xml_with_whitespace
|
||||||
|
mock_apply_template.return_value = mock_messages
|
||||||
|
|
||||||
|
state = PromptEnhancerState(prompt="Test prompt")
|
||||||
|
result = prompt_enhancer_node(state)
|
||||||
|
|
||||||
|
assert result == {"output": "Enhanced prompt with whitespace"}
|
||||||
|
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.apply_prompt_template")
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.get_llm_by_type")
|
||||||
|
@patch(
|
||||||
|
"src.prompt_enhancer.graph.enhancer_node.AGENT_LLM_MAP",
|
||||||
|
{"prompt_enhancer": "basic"},
|
||||||
|
)
|
||||||
|
def test_xml_multiline_content(
|
||||||
|
self, mock_get_llm, mock_apply_template, mock_llm_xml_multiline, mock_messages
|
||||||
|
):
|
||||||
|
"""Test XML extraction with multiline content."""
|
||||||
|
mock_get_llm.return_value = mock_llm_xml_multiline
|
||||||
|
mock_apply_template.return_value = mock_messages
|
||||||
|
|
||||||
|
state = PromptEnhancerState(prompt="Test prompt")
|
||||||
|
result = prompt_enhancer_node(state)
|
||||||
|
|
||||||
|
expected_output = """This is a multiline enhanced prompt
|
||||||
|
that spans multiple lines
|
||||||
|
and includes various formatting.
|
||||||
|
|
||||||
|
It should preserve the structure."""
|
||||||
|
assert result == {"output": expected_output}
|
||||||
|
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.apply_prompt_template")
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.get_llm_by_type")
|
||||||
|
@patch(
|
||||||
|
"src.prompt_enhancer.graph.enhancer_node.AGENT_LLM_MAP",
|
||||||
|
{"prompt_enhancer": "basic"},
|
||||||
|
)
|
||||||
|
def test_fallback_to_prefix_removal(
|
||||||
|
self, mock_get_llm, mock_apply_template, mock_llm_no_xml, mock_messages
|
||||||
|
):
|
||||||
|
"""Test fallback to prefix removal when no XML tags are found."""
|
||||||
|
mock_get_llm.return_value = mock_llm_no_xml
|
||||||
|
mock_apply_template.return_value = mock_messages
|
||||||
|
|
||||||
|
state = PromptEnhancerState(prompt="Test prompt")
|
||||||
|
result = prompt_enhancer_node(state)
|
||||||
|
|
||||||
|
assert result == {"output": "This is an enhanced prompt without XML tags"}
|
||||||
|
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.apply_prompt_template")
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.get_llm_by_type")
|
||||||
|
@patch(
|
||||||
|
"src.prompt_enhancer.graph.enhancer_node.AGENT_LLM_MAP",
|
||||||
|
{"prompt_enhancer": "basic"},
|
||||||
|
)
|
||||||
|
def test_malformed_xml_fallback(
|
||||||
|
self, mock_get_llm, mock_apply_template, mock_llm_malformed_xml, mock_messages
|
||||||
|
):
|
||||||
|
"""Test handling of malformed XML tags."""
|
||||||
|
mock_get_llm.return_value = mock_llm_malformed_xml
|
||||||
|
mock_apply_template.return_value = mock_messages
|
||||||
|
|
||||||
|
state = PromptEnhancerState(prompt="Test prompt")
|
||||||
|
result = prompt_enhancer_node(state)
|
||||||
|
|
||||||
|
# Should fall back to using the entire content since XML is malformed
|
||||||
|
expected_content = """<enhanced_prompt>
|
||||||
|
This XML tag is not properly closed
|
||||||
|
<enhanced_prompt>"""
|
||||||
|
assert result == {"output": expected_content}
|
||||||
|
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.apply_prompt_template")
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.get_llm_by_type")
|
||||||
|
@patch(
|
||||||
|
"src.prompt_enhancer.graph.enhancer_node.AGENT_LLM_MAP",
|
||||||
|
{"prompt_enhancer": "basic"},
|
||||||
|
)
|
||||||
|
def test_case_sensitive_prefix_removal(
|
||||||
|
self, mock_get_llm, mock_apply_template, mock_llm, mock_messages
|
||||||
|
):
|
||||||
|
"""Test that prefix removal is case-sensitive."""
|
||||||
|
mock_get_llm.return_value = mock_llm
|
||||||
|
mock_apply_template.return_value = mock_messages
|
||||||
|
|
||||||
|
# Test case variations that should NOT be removed
|
||||||
|
test_cases = [
|
||||||
|
"ENHANCED PROMPT: This should not be removed",
|
||||||
|
"enhanced prompt: This should not be removed",
|
||||||
|
"Enhanced Prompt This should not be removed", # Missing colon
|
||||||
|
"Enhanced Prompt :: This should not be removed", # Double colon
|
||||||
|
]
|
||||||
|
|
||||||
|
for response_content in test_cases:
|
||||||
|
mock_llm.invoke.return_value = MagicMock(content=response_content)
|
||||||
|
|
||||||
|
state = PromptEnhancerState(prompt="Test prompt")
|
||||||
|
result = prompt_enhancer_node(state)
|
||||||
|
|
||||||
|
# Should return the full content since prefix doesn't match exactly
|
||||||
|
assert result == {"output": response_content}
|
||||||
|
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.apply_prompt_template")
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.get_llm_by_type")
|
||||||
|
@patch(
|
||||||
|
"src.prompt_enhancer.graph.enhancer_node.AGENT_LLM_MAP",
|
||||||
|
{"prompt_enhancer": "basic"},
|
||||||
|
)
|
||||||
|
def test_prefix_with_extra_whitespace(
|
||||||
|
self, mock_get_llm, mock_apply_template, mock_llm, mock_messages
|
||||||
|
):
|
||||||
|
"""Test prefix removal with extra whitespace after colon."""
|
||||||
|
mock_get_llm.return_value = mock_llm
|
||||||
|
mock_apply_template.return_value = mock_messages
|
||||||
|
|
||||||
|
test_cases = [
|
||||||
|
("Enhanced Prompt: This has extra spaces", "This has extra spaces"),
|
||||||
|
("Enhanced prompt:\t\tThis has tabs", "This has tabs"),
|
||||||
|
("Here's the enhanced prompt:\n\nThis has newlines", "This has newlines"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for response_content, expected_output in test_cases:
|
||||||
|
mock_llm.invoke.return_value = MagicMock(content=response_content)
|
||||||
|
|
||||||
|
state = PromptEnhancerState(prompt="Test prompt")
|
||||||
|
result = prompt_enhancer_node(state)
|
||||||
|
|
||||||
|
assert result == {"output": expected_output}
|
||||||
|
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.apply_prompt_template")
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.get_llm_by_type")
|
||||||
|
@patch(
|
||||||
|
"src.prompt_enhancer.graph.enhancer_node.AGENT_LLM_MAP",
|
||||||
|
{"prompt_enhancer": "basic"},
|
||||||
|
)
|
||||||
|
def test_xml_with_special_characters(
|
||||||
|
self, mock_get_llm, mock_apply_template, mock_llm, mock_messages
|
||||||
|
):
|
||||||
|
"""Test XML extraction with special characters and symbols."""
|
||||||
|
mock_get_llm.return_value = mock_llm
|
||||||
|
mock_apply_template.return_value = mock_messages
|
||||||
|
|
||||||
|
special_content = """<enhanced_prompt>
|
||||||
|
Enhanced prompt with special chars: @#$%^&*()
|
||||||
|
Unicode: 🚀 ✨ 💡
|
||||||
|
Quotes: "double" and 'single'
|
||||||
|
Backslashes: \\n \\t \\r
|
||||||
|
</enhanced_prompt>"""
|
||||||
|
|
||||||
|
mock_llm.invoke.return_value = MagicMock(content=special_content)
|
||||||
|
|
||||||
|
state = PromptEnhancerState(prompt="Test prompt")
|
||||||
|
result = prompt_enhancer_node(state)
|
||||||
|
|
||||||
|
expected_output = """Enhanced prompt with special chars: @#$%^&*()
|
||||||
|
Unicode: 🚀 ✨ 💡
|
||||||
|
Quotes: "double" and 'single'
|
||||||
|
Backslashes: \\n \\t \\r"""
|
||||||
|
assert result == {"output": expected_output}
|
||||||
|
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.apply_prompt_template")
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.get_llm_by_type")
|
||||||
|
@patch(
|
||||||
|
"src.prompt_enhancer.graph.enhancer_node.AGENT_LLM_MAP",
|
||||||
|
{"prompt_enhancer": "basic"},
|
||||||
|
)
|
||||||
|
def test_very_long_response(
|
||||||
|
self, mock_get_llm, mock_apply_template, mock_llm, mock_messages
|
||||||
|
):
|
||||||
|
"""Test handling of very long LLM responses."""
|
||||||
|
mock_get_llm.return_value = mock_llm
|
||||||
|
mock_apply_template.return_value = mock_messages
|
||||||
|
|
||||||
|
# Create a very long response
|
||||||
|
long_content = "This is a very long enhanced prompt. " * 100
|
||||||
|
xml_response = f"<enhanced_prompt>\n{long_content}\n</enhanced_prompt>"
|
||||||
|
|
||||||
|
mock_llm.invoke.return_value = MagicMock(content=xml_response)
|
||||||
|
|
||||||
|
state = PromptEnhancerState(prompt="Test prompt")
|
||||||
|
result = prompt_enhancer_node(state)
|
||||||
|
|
||||||
|
assert result == {"output": long_content.strip()}
|
||||||
|
assert len(result["output"]) > 1000 # Verify it's actually long
|
||||||
|
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.apply_prompt_template")
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.get_llm_by_type")
|
||||||
|
@patch(
|
||||||
|
"src.prompt_enhancer.graph.enhancer_node.AGENT_LLM_MAP",
|
||||||
|
{"prompt_enhancer": "basic"},
|
||||||
|
)
|
||||||
|
def test_empty_response_content(
|
||||||
|
self, mock_get_llm, mock_apply_template, mock_llm, mock_messages
|
||||||
|
):
|
||||||
|
"""Test handling of empty response content."""
|
||||||
|
mock_get_llm.return_value = mock_llm
|
||||||
|
mock_apply_template.return_value = mock_messages
|
||||||
|
|
||||||
|
mock_llm.invoke.return_value = MagicMock(content="")
|
||||||
|
|
||||||
|
state = PromptEnhancerState(prompt="Test prompt")
|
||||||
|
result = prompt_enhancer_node(state)
|
||||||
|
|
||||||
|
assert result == {"output": ""}
|
||||||
|
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.apply_prompt_template")
|
||||||
|
@patch("src.prompt_enhancer.graph.enhancer_node.get_llm_by_type")
|
||||||
|
@patch(
|
||||||
|
"src.prompt_enhancer.graph.enhancer_node.AGENT_LLM_MAP",
|
||||||
|
{"prompt_enhancer": "basic"},
|
||||||
|
)
|
||||||
|
def test_only_whitespace_response(
|
||||||
|
self, mock_get_llm, mock_apply_template, mock_llm, mock_messages
|
||||||
|
):
|
||||||
|
"""Test handling of response with only whitespace."""
|
||||||
|
mock_get_llm.return_value = mock_llm
|
||||||
|
mock_apply_template.return_value = mock_messages
|
||||||
|
|
||||||
|
mock_llm.invoke.return_value = MagicMock(content=" \n\n\t\t ")
|
||||||
|
|
||||||
|
state = PromptEnhancerState(prompt="Test prompt")
|
||||||
|
result = prompt_enhancer_node(state)
|
||||||
|
|
||||||
|
assert result == {"output": ""}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import pytest
|
|
||||||
from src.prompt_enhancer.graph.state import PromptEnhancerState
|
from src.prompt_enhancer.graph.state import PromptEnhancerState
|
||||||
from src.config.report_style import ReportStyle
|
from src.config.report_style import ReportStyle
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
from src.rag.ragflow import RAGFlowProvider, parse_uri
|
||||||
|
|
||||||
|
|
||||||
|
# Dummy classes to mock dependencies
|
||||||
|
class DummyResource:
|
||||||
|
def __init__(self, uri, title="", description=""):
|
||||||
|
self.uri = uri
|
||||||
|
self.title = title
|
||||||
|
self.description = description
|
||||||
|
|
||||||
|
|
||||||
|
class DummyChunk:
|
||||||
|
def __init__(self, content, similarity):
|
||||||
|
self.content = content
|
||||||
|
self.similarity = similarity
|
||||||
|
|
||||||
|
|
||||||
|
class DummyDocument:
|
||||||
|
def __init__(self, id, title, chunks=None):
|
||||||
|
self.id = id
|
||||||
|
self.title = title
|
||||||
|
self.chunks = chunks or []
|
||||||
|
|
||||||
|
|
||||||
|
# Patch imports in ragflow.py to use dummy classes
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def patch_imports(monkeypatch):
|
||||||
|
import src.rag.ragflow as ragflow
|
||||||
|
|
||||||
|
ragflow.Resource = DummyResource
|
||||||
|
ragflow.Chunk = DummyChunk
|
||||||
|
ragflow.Document = DummyDocument
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_uri_valid():
|
||||||
|
uri = "rag://dataset/123#abc"
|
||||||
|
dataset_id, document_id = parse_uri(uri)
|
||||||
|
assert dataset_id == "123"
|
||||||
|
assert document_id == "abc"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_uri_invalid():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
parse_uri("http://dataset/123#abc")
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_env_vars(monkeypatch):
|
||||||
|
monkeypatch.setenv("RAGFLOW_API_URL", "http://api")
|
||||||
|
monkeypatch.setenv("RAGFLOW_API_KEY", "key")
|
||||||
|
monkeypatch.delenv("RAGFLOW_PAGE_SIZE", raising=False)
|
||||||
|
provider = RAGFlowProvider()
|
||||||
|
assert provider.api_url == "http://api"
|
||||||
|
assert provider.api_key == "key"
|
||||||
|
assert provider.page_size == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_page_size(monkeypatch):
|
||||||
|
monkeypatch.setenv("RAGFLOW_API_URL", "http://api")
|
||||||
|
monkeypatch.setenv("RAGFLOW_API_KEY", "key")
|
||||||
|
monkeypatch.setenv("RAGFLOW_PAGE_SIZE", "5")
|
||||||
|
provider = RAGFlowProvider()
|
||||||
|
assert provider.page_size == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_missing_env(monkeypatch):
|
||||||
|
monkeypatch.delenv("RAGFLOW_API_URL", raising=False)
|
||||||
|
monkeypatch.setenv("RAGFLOW_API_KEY", "key")
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
RAGFlowProvider()
|
||||||
|
monkeypatch.setenv("RAGFLOW_API_URL", "http://api")
|
||||||
|
monkeypatch.delenv("RAGFLOW_API_KEY", raising=False)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
RAGFlowProvider()
|
||||||
|
|
||||||
|
|
||||||
|
@patch("src.rag.ragflow.requests.post")
|
||||||
|
def test_query_relevant_documents_success(mock_post, monkeypatch):
|
||||||
|
monkeypatch.setenv("RAGFLOW_API_URL", "http://api")
|
||||||
|
monkeypatch.setenv("RAGFLOW_API_KEY", "key")
|
||||||
|
provider = RAGFlowProvider()
|
||||||
|
resource = DummyResource("rag://dataset/123#doc456")
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
"data": {
|
||||||
|
"doc_aggs": [{"doc_id": "doc456", "doc_name": "Doc Title"}],
|
||||||
|
"chunks": [
|
||||||
|
{"document_id": "doc456", "content": "chunk text", "similarity": 0.9}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
docs = provider.query_relevant_documents("query", [resource])
|
||||||
|
assert len(docs) == 1
|
||||||
|
assert docs[0].id == "doc456"
|
||||||
|
assert docs[0].title == "Doc Title"
|
||||||
|
assert len(docs[0].chunks) == 1
|
||||||
|
assert docs[0].chunks[0].content == "chunk text"
|
||||||
|
assert docs[0].chunks[0].similarity == 0.9
|
||||||
|
|
||||||
|
|
||||||
|
@patch("src.rag.ragflow.requests.post")
|
||||||
|
def test_query_relevant_documents_error(mock_post, monkeypatch):
|
||||||
|
monkeypatch.setenv("RAGFLOW_API_URL", "http://api")
|
||||||
|
monkeypatch.setenv("RAGFLOW_API_KEY", "key")
|
||||||
|
provider = RAGFlowProvider()
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 400
|
||||||
|
mock_response.text = "error"
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
provider.query_relevant_documents("query", [])
|
||||||
|
|
||||||
|
|
||||||
|
@patch("src.rag.ragflow.requests.get")
|
||||||
|
def test_list_resources_success(mock_get, monkeypatch):
|
||||||
|
monkeypatch.setenv("RAGFLOW_API_URL", "http://api")
|
||||||
|
monkeypatch.setenv("RAGFLOW_API_KEY", "key")
|
||||||
|
provider = RAGFlowProvider()
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
"data": [
|
||||||
|
{"id": "123", "name": "Dataset1", "description": "desc1"},
|
||||||
|
{"id": "456", "name": "Dataset2", "description": "desc2"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
resources = provider.list_resources()
|
||||||
|
assert len(resources) == 2
|
||||||
|
assert resources[0].uri == "rag://dataset/123"
|
||||||
|
assert resources[0].title == "Dataset1"
|
||||||
|
assert resources[0].description == "desc1"
|
||||||
|
assert resources[1].uri == "rag://dataset/456"
|
||||||
|
assert resources[1].title == "Dataset2"
|
||||||
|
assert resources[1].description == "desc2"
|
||||||
|
|
||||||
|
|
||||||
|
@patch("src.rag.ragflow.requests.get")
|
||||||
|
def test_list_resources_error(mock_get, monkeypatch):
|
||||||
|
monkeypatch.setenv("RAGFLOW_API_URL", "http://api")
|
||||||
|
monkeypatch.setenv("RAGFLOW_API_KEY", "key")
|
||||||
|
provider = RAGFlowProvider()
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 500
|
||||||
|
mock_response.text = "fail"
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
provider.list_resources()
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from src.rag.retriever import Chunk, Document, Resource, Retriever
|
||||||
|
|
||||||
|
|
||||||
|
def test_chunk_init():
|
||||||
|
chunk = Chunk(content="test content", similarity=0.9)
|
||||||
|
assert chunk.content == "test content"
|
||||||
|
assert chunk.similarity == 0.9
|
||||||
|
|
||||||
|
|
||||||
|
def test_document_init_and_to_dict():
|
||||||
|
chunk1 = Chunk(content="chunk1", similarity=0.8)
|
||||||
|
chunk2 = Chunk(content="chunk2", similarity=0.7)
|
||||||
|
doc = Document(
|
||||||
|
id="doc1", url="http://example.com", title="Title", chunks=[chunk1, chunk2]
|
||||||
|
)
|
||||||
|
assert doc.id == "doc1"
|
||||||
|
assert doc.url == "http://example.com"
|
||||||
|
assert doc.title == "Title"
|
||||||
|
assert doc.chunks == [chunk1, chunk2]
|
||||||
|
d = doc.to_dict()
|
||||||
|
assert d["id"] == "doc1"
|
||||||
|
assert d["content"] == "chunk1\n\nchunk2"
|
||||||
|
assert d["url"] == "http://example.com"
|
||||||
|
assert d["title"] == "Title"
|
||||||
|
|
||||||
|
|
||||||
|
def test_document_to_dict_optional_fields():
|
||||||
|
chunk = Chunk(content="only chunk", similarity=1.0)
|
||||||
|
doc = Document(id="doc2", chunks=[chunk])
|
||||||
|
d = doc.to_dict()
|
||||||
|
assert d["id"] == "doc2"
|
||||||
|
assert d["content"] == "only chunk"
|
||||||
|
assert "url" not in d
|
||||||
|
assert "title" not in d
|
||||||
|
|
||||||
|
|
||||||
|
def test_resource_model():
|
||||||
|
resource = Resource(uri="uri1", title="Resource Title")
|
||||||
|
assert resource.uri == "uri1"
|
||||||
|
assert resource.title == "Resource Title"
|
||||||
|
assert resource.description == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_resource_model_with_description():
|
||||||
|
resource = Resource(uri="uri2", title="Resource2", description="desc")
|
||||||
|
assert resource.description == "desc"
|
||||||
|
|
||||||
|
|
||||||
|
def test_retriever_abstract_methods():
|
||||||
|
class DummyRetriever(Retriever):
|
||||||
|
def list_resources(self, query=None):
|
||||||
|
return [Resource(uri="uri", title="title")]
|
||||||
|
|
||||||
|
def query_relevant_documents(self, query, resources=[]):
|
||||||
|
return [Document(id="id", chunks=[])]
|
||||||
|
|
||||||
|
retriever = DummyRetriever()
|
||||||
|
resources = retriever.list_resources()
|
||||||
|
assert isinstance(resources, list)
|
||||||
|
assert isinstance(resources[0], Resource)
|
||||||
|
docs = retriever.query_relevant_documents("query", resources)
|
||||||
|
assert isinstance(docs, list)
|
||||||
|
assert isinstance(docs[0], Document)
|
||||||
|
|
||||||
|
|
||||||
|
def test_retriever_cannot_instantiate():
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
Retriever()
|
||||||
@@ -0,0 +1,503 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
import json
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
from src.rag.vikingdb_knowledge_base import VikingDBKnowledgeBaseProvider, parse_uri
|
||||||
|
|
||||||
|
|
||||||
|
# Dummy classes to mock dependencies
|
||||||
|
class MockResource:
|
||||||
|
def __init__(self, uri, title="", description=""):
|
||||||
|
self.uri = uri
|
||||||
|
self.title = title
|
||||||
|
self.description = description
|
||||||
|
|
||||||
|
|
||||||
|
class MockChunk:
|
||||||
|
def __init__(self, content, similarity):
|
||||||
|
self.content = content
|
||||||
|
self.similarity = similarity
|
||||||
|
|
||||||
|
|
||||||
|
class MockDocument:
|
||||||
|
def __init__(self, id, title, chunks=None):
|
||||||
|
self.id = id
|
||||||
|
self.title = title
|
||||||
|
self.chunks = chunks or []
|
||||||
|
|
||||||
|
|
||||||
|
# Patch the imports to use mock classes
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def patch_imports():
|
||||||
|
with (
|
||||||
|
patch("src.rag.vikingdb_knowledge_base.Resource", MockResource),
|
||||||
|
patch("src.rag.vikingdb_knowledge_base.Chunk", MockChunk),
|
||||||
|
patch("src.rag.vikingdb_knowledge_base.Document", MockDocument),
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def env_vars():
|
||||||
|
"""Fixture to set up environment variables"""
|
||||||
|
with patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"VIKINGDB_KNOWLEDGE_BASE_API_URL": "api-test.example.com",
|
||||||
|
"VIKINGDB_KNOWLEDGE_BASE_API_AK": "test_ak",
|
||||||
|
"VIKINGDB_KNOWLEDGE_BASE_API_SK": "test_sk",
|
||||||
|
"VIKINGDB_KNOWLEDGE_BASE_RETRIEVAL_SIZE": "10",
|
||||||
|
},
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseUri:
|
||||||
|
def test_parse_uri_valid_with_fragment(self):
|
||||||
|
"""Test parsing valid URI with fragment"""
|
||||||
|
uri = "rag://dataset/123#doc456"
|
||||||
|
resource_id, document_id = parse_uri(uri)
|
||||||
|
assert resource_id == "123"
|
||||||
|
assert document_id == "doc456"
|
||||||
|
|
||||||
|
def test_parse_uri_valid_without_fragment(self):
|
||||||
|
"""Test parsing valid URI without fragment"""
|
||||||
|
uri = "rag://dataset/123"
|
||||||
|
resource_id, document_id = parse_uri(uri)
|
||||||
|
assert resource_id == "123"
|
||||||
|
assert document_id == ""
|
||||||
|
|
||||||
|
def test_parse_uri_invalid_scheme(self):
|
||||||
|
"""Test parsing URI with invalid scheme"""
|
||||||
|
with pytest.raises(ValueError, match="Invalid URI"):
|
||||||
|
parse_uri("http://dataset/123#abc")
|
||||||
|
|
||||||
|
def test_parse_uri_malformed(self):
|
||||||
|
"""Test parsing malformed URI"""
|
||||||
|
with pytest.raises(ValueError, match="Invalid URI"):
|
||||||
|
parse_uri("invalid_uri")
|
||||||
|
|
||||||
|
|
||||||
|
class TestVikingDBKnowledgeBaseProviderInit:
|
||||||
|
def test_init_success_with_all_env_vars(self, env_vars):
|
||||||
|
"""Test successful initialization with all environment variables"""
|
||||||
|
provider = VikingDBKnowledgeBaseProvider()
|
||||||
|
assert provider.api_url == "api-test.example.com"
|
||||||
|
assert provider.api_ak == "test_ak"
|
||||||
|
assert provider.api_sk == "test_sk"
|
||||||
|
assert provider.retrieval_size == 10
|
||||||
|
|
||||||
|
def test_init_success_without_retrieval_size(self):
|
||||||
|
"""Test initialization without VIKINGDB_KNOWLEDGE_BASE_RETRIEVAL_SIZE (should use default)"""
|
||||||
|
with patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"VIKINGDB_KNOWLEDGE_BASE_API_URL": "api-test.example.com",
|
||||||
|
"VIKINGDB_KNOWLEDGE_BASE_API_AK": "test_ak",
|
||||||
|
"VIKINGDB_KNOWLEDGE_BASE_API_SK": "test_sk",
|
||||||
|
},
|
||||||
|
clear=True,
|
||||||
|
):
|
||||||
|
provider = VikingDBKnowledgeBaseProvider()
|
||||||
|
assert provider.retrieval_size == 10
|
||||||
|
|
||||||
|
def test_init_custom_retrieval_size(self):
|
||||||
|
"""Test initialization with custom retrieval size"""
|
||||||
|
with patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"VIKINGDB_KNOWLEDGE_BASE_API_URL": "api-test.example.com",
|
||||||
|
"VIKINGDB_KNOWLEDGE_BASE_API_AK": "test_ak",
|
||||||
|
"VIKINGDB_KNOWLEDGE_BASE_API_SK": "test_sk",
|
||||||
|
"VIKINGDB_KNOWLEDGE_BASE_RETRIEVAL_SIZE": "5",
|
||||||
|
},
|
||||||
|
):
|
||||||
|
provider = VikingDBKnowledgeBaseProvider()
|
||||||
|
assert provider.retrieval_size == 5
|
||||||
|
|
||||||
|
def test_init_missing_api_url(self):
|
||||||
|
"""Test initialization fails when API URL is missing"""
|
||||||
|
with patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"VIKINGDB_KNOWLEDGE_BASE_API_AK": "test_ak",
|
||||||
|
"VIKINGDB_KNOWLEDGE_BASE_API_SK": "test_sk",
|
||||||
|
},
|
||||||
|
clear=True,
|
||||||
|
):
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError, match="VIKINGDB_KNOWLEDGE_BASE_API_URL is not set"
|
||||||
|
):
|
||||||
|
VikingDBKnowledgeBaseProvider()
|
||||||
|
|
||||||
|
def test_init_missing_api_ak(self):
|
||||||
|
"""Test initialization fails when API AK is missing"""
|
||||||
|
with patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"VIKINGDB_KNOWLEDGE_BASE_API_URL": "api-test.example.com",
|
||||||
|
"VIKINGDB_KNOWLEDGE_BASE_API_SK": "test_sk",
|
||||||
|
},
|
||||||
|
clear=True,
|
||||||
|
):
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError, match="VIKINGDB_KNOWLEDGE_BASE_API_AK is not set"
|
||||||
|
):
|
||||||
|
VikingDBKnowledgeBaseProvider()
|
||||||
|
|
||||||
|
def test_init_missing_api_sk(self):
|
||||||
|
"""Test initialization fails when API SK is missing"""
|
||||||
|
with patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"VIKINGDB_KNOWLEDGE_BASE_API_URL": "api-test.example.com",
|
||||||
|
"VIKINGDB_KNOWLEDGE_BASE_API_AK": "test_ak",
|
||||||
|
},
|
||||||
|
clear=True,
|
||||||
|
):
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError, match="VIKINGDB_KNOWLEDGE_BASE_API_SK is not set"
|
||||||
|
):
|
||||||
|
VikingDBKnowledgeBaseProvider()
|
||||||
|
|
||||||
|
|
||||||
|
class TestVikingDBKnowledgeBaseProviderPrepareRequest:
|
||||||
|
@pytest.fixture
|
||||||
|
def provider(self, env_vars):
|
||||||
|
return VikingDBKnowledgeBaseProvider()
|
||||||
|
|
||||||
|
def test_prepare_request_basic(self, provider):
|
||||||
|
"""Test basic request preparation"""
|
||||||
|
with (
|
||||||
|
patch("src.rag.vikingdb_knowledge_base.Request") as mock_request,
|
||||||
|
patch("src.rag.vikingdb_knowledge_base.Credentials") as _mock_credentials,
|
||||||
|
patch("src.rag.vikingdb_knowledge_base.SignerV4.sign") as _mock_sign,
|
||||||
|
):
|
||||||
|
|
||||||
|
mock_req_instance = MagicMock()
|
||||||
|
mock_request.return_value = mock_req_instance
|
||||||
|
|
||||||
|
result = provider.prepare_request("POST", "/test/path")
|
||||||
|
|
||||||
|
assert result == mock_req_instance
|
||||||
|
mock_req_instance.set_shema.assert_called_once_with("https")
|
||||||
|
mock_req_instance.set_method.assert_called_once_with("POST")
|
||||||
|
mock_req_instance.set_path.assert_called_once_with("/test/path")
|
||||||
|
|
||||||
|
def test_prepare_request_with_params(self, provider):
|
||||||
|
"""Test request preparation with parameters"""
|
||||||
|
with (
|
||||||
|
patch("src.rag.vikingdb_knowledge_base.Request") as mock_request,
|
||||||
|
patch("src.rag.vikingdb_knowledge_base.Credentials"),
|
||||||
|
patch("src.rag.vikingdb_knowledge_base.SignerV4.sign"),
|
||||||
|
):
|
||||||
|
|
||||||
|
mock_req_instance = MagicMock()
|
||||||
|
mock_request.return_value = mock_req_instance
|
||||||
|
|
||||||
|
params = {"key": "value", "number": 123, "boolean": True}
|
||||||
|
provider.prepare_request("GET", "/test", params=params)
|
||||||
|
|
||||||
|
expected_params = {"key": "value", "number": "123", "boolean": "True"}
|
||||||
|
mock_req_instance.set_query.assert_called_once_with(expected_params)
|
||||||
|
|
||||||
|
def test_prepare_request_with_data(self, provider):
|
||||||
|
"""Test request preparation with data"""
|
||||||
|
with (
|
||||||
|
patch("src.rag.vikingdb_knowledge_base.Request") as mock_request,
|
||||||
|
patch("src.rag.vikingdb_knowledge_base.Credentials"),
|
||||||
|
patch("src.rag.vikingdb_knowledge_base.SignerV4.sign"),
|
||||||
|
):
|
||||||
|
|
||||||
|
mock_req_instance = MagicMock()
|
||||||
|
mock_request.return_value = mock_req_instance
|
||||||
|
|
||||||
|
data = {"test": "data"}
|
||||||
|
provider.prepare_request("POST", "/test", data=data)
|
||||||
|
|
||||||
|
mock_req_instance.set_body.assert_called_once_with(json.dumps(data))
|
||||||
|
|
||||||
|
|
||||||
|
class TestVikingDBKnowledgeBaseProviderQueryRelevantDocuments:
|
||||||
|
@pytest.fixture
|
||||||
|
def provider(self, env_vars):
|
||||||
|
return VikingDBKnowledgeBaseProvider()
|
||||||
|
|
||||||
|
def test_query_relevant_documents_empty_resources(self, provider):
|
||||||
|
"""Test querying with empty resources list"""
|
||||||
|
result = provider.query_relevant_documents("test query", [])
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
@patch("src.rag.vikingdb_knowledge_base.requests.request")
|
||||||
|
def test_query_relevant_documents_success(self, mock_request, provider):
|
||||||
|
"""Test successful document query"""
|
||||||
|
# Mock response
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.text = json.dumps(
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"result_list": [
|
||||||
|
{
|
||||||
|
"doc_info": {
|
||||||
|
"doc_id": "doc123",
|
||||||
|
"doc_name": "Test Document",
|
||||||
|
},
|
||||||
|
"content": "Test content",
|
||||||
|
"score": 0.95,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
mock_request.return_value = mock_response
|
||||||
|
|
||||||
|
# Mock prepare_request
|
||||||
|
with patch.object(provider, "prepare_request") as mock_prepare:
|
||||||
|
mock_req = MagicMock()
|
||||||
|
mock_req.method = "POST"
|
||||||
|
mock_req.path = "/api/knowledge/collection/search_knowledge"
|
||||||
|
mock_req.headers = {}
|
||||||
|
mock_req.body = "{}"
|
||||||
|
mock_prepare.return_value = mock_req
|
||||||
|
|
||||||
|
resources = [MockResource("rag://dataset/123")]
|
||||||
|
result = provider.query_relevant_documents("test query", resources)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].id == "doc123"
|
||||||
|
assert result[0].title == "Test Document"
|
||||||
|
assert len(result[0].chunks) == 1
|
||||||
|
assert result[0].chunks[0].content == "Test content"
|
||||||
|
assert result[0].chunks[0].similarity == 0.95
|
||||||
|
|
||||||
|
@patch("src.rag.vikingdb_knowledge_base.requests.request")
|
||||||
|
def test_query_relevant_documents_with_document_filter(
|
||||||
|
self, mock_request, provider
|
||||||
|
):
|
||||||
|
"""Test document query with document ID filter"""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.text = json.dumps({"code": 0, "data": {"result_list": []}})
|
||||||
|
mock_request.return_value = mock_response
|
||||||
|
|
||||||
|
with patch.object(provider, "prepare_request") as mock_prepare:
|
||||||
|
mock_req = MagicMock()
|
||||||
|
mock_prepare.return_value = mock_req
|
||||||
|
|
||||||
|
resources = [MockResource("rag://dataset/123#doc456")]
|
||||||
|
provider.query_relevant_documents("test query", resources)
|
||||||
|
|
||||||
|
# Verify that query_param with doc_filter was included in the request
|
||||||
|
call_args = mock_prepare.call_args
|
||||||
|
request_data = call_args[1]["data"]
|
||||||
|
assert "query_param" in request_data
|
||||||
|
assert "doc_filter" in request_data["query_param"]
|
||||||
|
|
||||||
|
doc_filter = request_data["query_param"]["doc_filter"]
|
||||||
|
assert doc_filter["op"] == "must"
|
||||||
|
assert doc_filter["field"] == "doc_id"
|
||||||
|
assert doc_filter["conds"] == ["doc456"]
|
||||||
|
|
||||||
|
@patch("src.rag.vikingdb_knowledge_base.requests.request")
|
||||||
|
def test_query_relevant_documents_api_error(self, mock_request, provider):
|
||||||
|
"""Test handling of API error response"""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.text = json.dumps({"code": 1, "message": "API Error"})
|
||||||
|
mock_request.return_value = mock_response
|
||||||
|
|
||||||
|
with patch.object(provider, "prepare_request"):
|
||||||
|
resources = [MockResource("rag://dataset/123")]
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError, match="Failed to query documents from resource: API Error"
|
||||||
|
):
|
||||||
|
provider.query_relevant_documents("test query", resources)
|
||||||
|
|
||||||
|
@patch("src.rag.vikingdb_knowledge_base.requests.request")
|
||||||
|
def test_query_relevant_documents_json_decode_error(self, mock_request, provider):
|
||||||
|
"""Test handling of JSON decode error"""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.text = "invalid json"
|
||||||
|
mock_request.return_value = mock_response
|
||||||
|
|
||||||
|
with patch.object(provider, "prepare_request"):
|
||||||
|
resources = [MockResource("rag://dataset/123")]
|
||||||
|
with pytest.raises(ValueError, match="Failed to parse JSON response"):
|
||||||
|
provider.query_relevant_documents("test query", resources)
|
||||||
|
|
||||||
|
@patch("src.rag.vikingdb_knowledge_base.requests.request")
|
||||||
|
def test_query_relevant_documents_multiple_resources(self, mock_request, provider):
|
||||||
|
"""Test querying multiple resources and merging results"""
|
||||||
|
# Mock responses for different resources
|
||||||
|
responses = [
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"result_list": [
|
||||||
|
{
|
||||||
|
"doc_info": {
|
||||||
|
"doc_id": "doc1",
|
||||||
|
"doc_name": "Document 1",
|
||||||
|
},
|
||||||
|
"content": "Content 1",
|
||||||
|
"score": 0.9,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"result_list": [
|
||||||
|
{
|
||||||
|
"doc_info": {
|
||||||
|
"doc_id": "doc1",
|
||||||
|
"doc_name": "Document 1",
|
||||||
|
},
|
||||||
|
"content": "Content 2",
|
||||||
|
"score": 0.8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"doc_info": {
|
||||||
|
"doc_id": "doc2",
|
||||||
|
"doc_name": "Document 2",
|
||||||
|
},
|
||||||
|
"content": "Content 3",
|
||||||
|
"score": 0.7,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
mock_request.side_effect = [MagicMock(text=resp) for resp in responses]
|
||||||
|
|
||||||
|
with patch.object(provider, "prepare_request"):
|
||||||
|
resources = [
|
||||||
|
MockResource("rag://dataset/123"),
|
||||||
|
MockResource("rag://dataset/456"),
|
||||||
|
]
|
||||||
|
result = provider.query_relevant_documents("test query", resources)
|
||||||
|
|
||||||
|
# Should have 2 documents: doc1 (with 2 chunks) and doc2 (with 1 chunk)
|
||||||
|
assert len(result) == 2
|
||||||
|
doc1 = next(doc for doc in result if doc.id == "doc1")
|
||||||
|
doc2 = next(doc for doc in result if doc.id == "doc2")
|
||||||
|
assert len(doc1.chunks) == 2
|
||||||
|
assert len(doc2.chunks) == 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestVikingDBKnowledgeBaseProviderListResources:
|
||||||
|
@pytest.fixture
|
||||||
|
def provider(self, env_vars):
|
||||||
|
return VikingDBKnowledgeBaseProvider()
|
||||||
|
|
||||||
|
@patch("src.rag.vikingdb_knowledge_base.requests.request")
|
||||||
|
def test_list_resources_success(self, mock_request, provider):
|
||||||
|
"""Test successful resource listing"""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.text = json.dumps(
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"collection_list": [
|
||||||
|
{
|
||||||
|
"resource_id": "123",
|
||||||
|
"collection_name": "Dataset 1",
|
||||||
|
"description": "Description 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resource_id": "456",
|
||||||
|
"collection_name": "Dataset 2",
|
||||||
|
"description": "Description 2",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
mock_request.return_value = mock_response
|
||||||
|
|
||||||
|
with patch.object(provider, "prepare_request") as mock_prepare:
|
||||||
|
mock_req = MagicMock()
|
||||||
|
mock_prepare.return_value = mock_req
|
||||||
|
|
||||||
|
result = provider.list_resources()
|
||||||
|
|
||||||
|
assert len(result) == 2
|
||||||
|
assert result[0].uri == "rag://dataset/123"
|
||||||
|
assert result[0].title == "Dataset 1"
|
||||||
|
assert result[0].description == "Description 1"
|
||||||
|
assert result[1].uri == "rag://dataset/456"
|
||||||
|
assert result[1].title == "Dataset 2"
|
||||||
|
assert result[1].description == "Description 2"
|
||||||
|
|
||||||
|
@patch("src.rag.vikingdb_knowledge_base.requests.request")
|
||||||
|
def test_list_resources_with_query_filter(self, mock_request, provider):
|
||||||
|
"""Test resource listing with query filter"""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.text = json.dumps(
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"collection_list": [
|
||||||
|
{
|
||||||
|
"resource_id": "123",
|
||||||
|
"collection_name": "Test Dataset",
|
||||||
|
"description": "Description",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resource_id": "456",
|
||||||
|
"collection_name": "Other Dataset",
|
||||||
|
"description": "Description",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
mock_request.return_value = mock_response
|
||||||
|
|
||||||
|
with patch.object(provider, "prepare_request"):
|
||||||
|
result = provider.list_resources("test")
|
||||||
|
|
||||||
|
# Should only return the dataset with "test" in the name
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].title == "Test Dataset"
|
||||||
|
|
||||||
|
@patch("src.rag.vikingdb_knowledge_base.requests.request")
|
||||||
|
def test_list_resources_api_error(self, mock_request, provider):
|
||||||
|
"""Test handling of API error in list_resources"""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.text = json.dumps({"code": 1, "message": "API Error"})
|
||||||
|
mock_request.return_value = mock_response
|
||||||
|
|
||||||
|
with patch.object(provider, "prepare_request"):
|
||||||
|
with pytest.raises(Exception, match="Failed to list resources: API Error"):
|
||||||
|
provider.list_resources()
|
||||||
|
|
||||||
|
@patch("src.rag.vikingdb_knowledge_base.requests.request")
|
||||||
|
def test_list_resources_json_decode_error(self, mock_request, provider):
|
||||||
|
"""Test handling of JSON decode error in list_resources"""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.text = "invalid json"
|
||||||
|
mock_request.return_value = mock_response
|
||||||
|
|
||||||
|
with patch.object(provider, "prepare_request"):
|
||||||
|
with pytest.raises(ValueError, match="Failed to parse JSON response"):
|
||||||
|
provider.list_resources()
|
||||||
|
|
||||||
|
@patch("src.rag.vikingdb_knowledge_base.requests.request")
|
||||||
|
def test_list_resources_empty_response(self, mock_request, provider):
|
||||||
|
"""Test handling of empty response"""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.text = json.dumps({"code": 0, "data": {"collection_list": []}})
|
||||||
|
mock_request.return_value = mock_response
|
||||||
|
|
||||||
|
with patch.object(provider, "prepare_request"):
|
||||||
|
result = provider.list_resources()
|
||||||
|
assert result == []
|
||||||
@@ -0,0 +1,722 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
from unittest.mock import MagicMock, patch, mock_open
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from src.server.app import app, _make_event, _astream_workflow_generator
|
||||||
|
from src.config.report_style import ReportStyle
|
||||||
|
from langgraph.types import Command
|
||||||
|
from langchain_core.messages import ToolMessage
|
||||||
|
from langchain_core.messages import AIMessageChunk
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMakeEvent:
|
||||||
|
def test_make_event_with_content(self):
|
||||||
|
event_type = "message_chunk"
|
||||||
|
data = {"content": "Hello", "role": "assistant"}
|
||||||
|
result = _make_event(event_type, data)
|
||||||
|
expected = (
|
||||||
|
'event: message_chunk\ndata: {"content": "Hello", "role": "assistant"}\n\n'
|
||||||
|
)
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
def test_make_event_with_empty_content(self):
|
||||||
|
event_type = "message_chunk"
|
||||||
|
data = {"content": "", "role": "assistant"}
|
||||||
|
result = _make_event(event_type, data)
|
||||||
|
expected = 'event: message_chunk\ndata: {"role": "assistant"}\n\n'
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
def test_make_event_without_content(self):
|
||||||
|
event_type = "tool_calls"
|
||||||
|
data = {"role": "assistant", "tool_calls": []}
|
||||||
|
result = _make_event(event_type, data)
|
||||||
|
expected = (
|
||||||
|
'event: tool_calls\ndata: {"role": "assistant", "tool_calls": []}\n\n'
|
||||||
|
)
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
class TestTTSEndpoint:
|
||||||
|
@patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"VOLCENGINE_TTS_APPID": "test_app_id",
|
||||||
|
"VOLCENGINE_TTS_ACCESS_TOKEN": "test_token",
|
||||||
|
"VOLCENGINE_TTS_CLUSTER": "test_cluster",
|
||||||
|
"VOLCENGINE_TTS_VOICE_TYPE": "test_voice",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@patch("src.server.app.VolcengineTTS")
|
||||||
|
def test_tts_success(self, mock_tts_class, client):
|
||||||
|
mock_tts_instance = MagicMock()
|
||||||
|
mock_tts_class.return_value = mock_tts_instance
|
||||||
|
|
||||||
|
# Mock successful TTS response
|
||||||
|
audio_data_b64 = base64.b64encode(b"fake_audio_data").decode()
|
||||||
|
mock_tts_instance.text_to_speech.return_value = {
|
||||||
|
"success": True,
|
||||||
|
"audio_data": audio_data_b64,
|
||||||
|
}
|
||||||
|
|
||||||
|
request_data = {
|
||||||
|
"text": "Hello world",
|
||||||
|
"encoding": "mp3",
|
||||||
|
"speed_ratio": 1.0,
|
||||||
|
"volume_ratio": 1.0,
|
||||||
|
"pitch_ratio": 1.0,
|
||||||
|
"text_type": "plain",
|
||||||
|
"with_frontend": True,
|
||||||
|
"frontend_type": "unitTson",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post("/api/tts", json=request_data)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.headers["content-type"] == "audio/mp3"
|
||||||
|
assert b"fake_audio_data" in response.content
|
||||||
|
|
||||||
|
@patch.dict(os.environ, {}, clear=True)
|
||||||
|
def test_tts_missing_app_id(self, client):
|
||||||
|
request_data = {"text": "Hello world", "encoding": "mp3"}
|
||||||
|
|
||||||
|
response = client.post("/api/tts", json=request_data)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert "VOLCENGINE_TTS_APPID is not set" in response.json()["detail"]
|
||||||
|
|
||||||
|
@patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{"VOLCENGINE_TTS_APPID": "test_app_id", "VOLCENGINE_TTS_ACCESS_TOKEN": ""},
|
||||||
|
)
|
||||||
|
def test_tts_missing_access_token(self, client):
|
||||||
|
request_data = {"text": "Hello world", "encoding": "mp3"}
|
||||||
|
|
||||||
|
response = client.post("/api/tts", json=request_data)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert "VOLCENGINE_TTS_ACCESS_TOKEN is not set" in response.json()["detail"]
|
||||||
|
|
||||||
|
@patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"VOLCENGINE_TTS_APPID": "test_app_id",
|
||||||
|
"VOLCENGINE_TTS_ACCESS_TOKEN": "test_token",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@patch("src.server.app.VolcengineTTS")
|
||||||
|
def test_tts_api_error(self, mock_tts_class, client):
|
||||||
|
mock_tts_instance = MagicMock()
|
||||||
|
mock_tts_class.return_value = mock_tts_instance
|
||||||
|
|
||||||
|
# Mock TTS error response
|
||||||
|
mock_tts_instance.text_to_speech.return_value = {
|
||||||
|
"success": False,
|
||||||
|
"error": "TTS API error",
|
||||||
|
}
|
||||||
|
|
||||||
|
request_data = {"text": "Hello world", "encoding": "mp3"}
|
||||||
|
|
||||||
|
response = client.post("/api/tts", json=request_data)
|
||||||
|
|
||||||
|
assert response.status_code == 500
|
||||||
|
assert "Internal Server Error" in response.json()["detail"]
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="TTS server exception is catched")
|
||||||
|
@patch("src.server.app.VolcengineTTS")
|
||||||
|
def test_tts_api_exception(self, mock_tts_class, client):
|
||||||
|
mock_tts_instance = MagicMock()
|
||||||
|
mock_tts_class.return_value = mock_tts_instance
|
||||||
|
|
||||||
|
# Mock TTS error response
|
||||||
|
mock_tts_instance.side_effect = Exception("TTS API error")
|
||||||
|
|
||||||
|
request_data = {"text": "Hello world", "encoding": "mp3"}
|
||||||
|
|
||||||
|
response = client.post("/api/tts", json=request_data)
|
||||||
|
|
||||||
|
assert response.status_code == 500
|
||||||
|
assert "Internal Server Error" in response.json()["detail"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestPodcastEndpoint:
|
||||||
|
@patch("src.server.app.build_podcast_graph")
|
||||||
|
def test_generate_podcast_success(self, mock_build_graph, client):
|
||||||
|
mock_workflow = MagicMock()
|
||||||
|
mock_build_graph.return_value = mock_workflow
|
||||||
|
mock_workflow.invoke.return_value = {"output": b"fake_audio_data"}
|
||||||
|
|
||||||
|
request_data = {"content": "Test content for podcast"}
|
||||||
|
|
||||||
|
response = client.post("/api/podcast/generate", json=request_data)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.headers["content-type"] == "audio/mp3"
|
||||||
|
assert response.content == b"fake_audio_data"
|
||||||
|
|
||||||
|
@patch("src.server.app.build_podcast_graph")
|
||||||
|
def test_generate_podcast_error(self, mock_build_graph, client):
|
||||||
|
mock_build_graph.side_effect = Exception("Podcast generation failed")
|
||||||
|
|
||||||
|
request_data = {"content": "Test content"}
|
||||||
|
|
||||||
|
response = client.post("/api/podcast/generate", json=request_data)
|
||||||
|
|
||||||
|
assert response.status_code == 500
|
||||||
|
assert response.json()["detail"] == "Internal Server Error"
|
||||||
|
|
||||||
|
|
||||||
|
class TestPPTEndpoint:
|
||||||
|
@patch("src.server.app.build_ppt_graph")
|
||||||
|
@patch("builtins.open", new_callable=mock_open, read_data=b"fake_ppt_data")
|
||||||
|
def test_generate_ppt_success(self, mock_file, mock_build_graph, client):
|
||||||
|
mock_workflow = MagicMock()
|
||||||
|
mock_build_graph.return_value = mock_workflow
|
||||||
|
mock_workflow.invoke.return_value = {
|
||||||
|
"generated_file_path": "/fake/path/test.pptx"
|
||||||
|
}
|
||||||
|
|
||||||
|
request_data = {"content": "Test content for PPT"}
|
||||||
|
|
||||||
|
response = client.post("/api/ppt/generate", json=request_data)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert (
|
||||||
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||||
|
in response.headers["content-type"]
|
||||||
|
)
|
||||||
|
assert response.content == b"fake_ppt_data"
|
||||||
|
|
||||||
|
@patch("src.server.app.build_ppt_graph")
|
||||||
|
def test_generate_ppt_error(self, mock_build_graph, client):
|
||||||
|
mock_build_graph.side_effect = Exception("PPT generation failed")
|
||||||
|
|
||||||
|
request_data = {"content": "Test content"}
|
||||||
|
|
||||||
|
response = client.post("/api/ppt/generate", json=request_data)
|
||||||
|
|
||||||
|
assert response.status_code == 500
|
||||||
|
assert response.json()["detail"] == "Internal Server Error"
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnhancePromptEndpoint:
|
||||||
|
@patch("src.server.app.build_prompt_enhancer_graph")
|
||||||
|
def test_enhance_prompt_success(self, mock_build_graph, client):
|
||||||
|
mock_workflow = MagicMock()
|
||||||
|
mock_build_graph.return_value = mock_workflow
|
||||||
|
mock_workflow.invoke.return_value = {"output": "Enhanced prompt"}
|
||||||
|
|
||||||
|
request_data = {
|
||||||
|
"prompt": "Original prompt",
|
||||||
|
"context": "Some context",
|
||||||
|
"report_style": "academic",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post("/api/prompt/enhance", json=request_data)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["result"] == "Enhanced prompt"
|
||||||
|
|
||||||
|
@patch("src.server.app.build_prompt_enhancer_graph")
|
||||||
|
def test_enhance_prompt_with_different_styles(self, mock_build_graph, client):
|
||||||
|
mock_workflow = MagicMock()
|
||||||
|
mock_build_graph.return_value = mock_workflow
|
||||||
|
mock_workflow.invoke.return_value = {"output": "Enhanced prompt"}
|
||||||
|
|
||||||
|
styles = [
|
||||||
|
"ACADEMIC",
|
||||||
|
"popular_science",
|
||||||
|
"NEWS",
|
||||||
|
"social_media",
|
||||||
|
"invalid_style",
|
||||||
|
]
|
||||||
|
|
||||||
|
for style in styles:
|
||||||
|
request_data = {"prompt": "Test prompt", "report_style": style}
|
||||||
|
|
||||||
|
response = client.post("/api/prompt/enhance", json=request_data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
@patch("src.server.app.build_prompt_enhancer_graph")
|
||||||
|
def test_enhance_prompt_error(self, mock_build_graph, client):
|
||||||
|
mock_build_graph.side_effect = Exception("Enhancement failed")
|
||||||
|
|
||||||
|
request_data = {"prompt": "Test prompt"}
|
||||||
|
|
||||||
|
response = client.post("/api/prompt/enhance", json=request_data)
|
||||||
|
|
||||||
|
assert response.status_code == 500
|
||||||
|
assert response.json()["detail"] == "Internal Server Error"
|
||||||
|
|
||||||
|
|
||||||
|
class TestMCPEndpoint:
|
||||||
|
@patch("src.server.app.load_mcp_tools")
|
||||||
|
def test_mcp_server_metadata_success(self, mock_load_tools, client):
|
||||||
|
mock_load_tools.return_value = [
|
||||||
|
{"name": "test_tool", "description": "Test tool"}
|
||||||
|
]
|
||||||
|
|
||||||
|
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 == 200
|
||||||
|
response_data = response.json()
|
||||||
|
assert response_data["transport"] == "stdio"
|
||||||
|
assert response_data["command"] == "test_command"
|
||||||
|
assert len(response_data["tools"]) == 1
|
||||||
|
|
||||||
|
@patch("src.server.app.load_mcp_tools")
|
||||||
|
def test_mcp_server_metadata_with_custom_timeout(self, mock_load_tools, client):
|
||||||
|
mock_load_tools.return_value = []
|
||||||
|
|
||||||
|
request_data = {
|
||||||
|
"transport": "stdio",
|
||||||
|
"command": "test_command",
|
||||||
|
"timeout_seconds": 600,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post("/api/mcp/server/metadata", json=request_data)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
mock_load_tools.assert_called_once()
|
||||||
|
|
||||||
|
@patch("src.server.app.load_mcp_tools")
|
||||||
|
def test_mcp_server_metadata_with_exception(self, mock_load_tools, client):
|
||||||
|
mock_load_tools.side_effect = HTTPException(
|
||||||
|
status_code=400, detail="MCP Server Error"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 == 500
|
||||||
|
assert response.json()["detail"] == "Internal Server Error"
|
||||||
|
|
||||||
|
|
||||||
|
class TestRAGEndpoints:
|
||||||
|
@patch("src.server.app.SELECTED_RAG_PROVIDER", "test_provider")
|
||||||
|
def test_rag_config(self, client):
|
||||||
|
response = client.get("/api/rag/config")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["provider"] == "test_provider"
|
||||||
|
|
||||||
|
@patch("src.server.app.build_retriever")
|
||||||
|
def test_rag_resources_with_retriever(self, mock_build_retriever, client):
|
||||||
|
mock_retriever = MagicMock()
|
||||||
|
mock_retriever.list_resources.return_value = [
|
||||||
|
{
|
||||||
|
"uri": "test_uri",
|
||||||
|
"title": "Test Resource",
|
||||||
|
"description": "Test Description",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
mock_build_retriever.return_value = mock_retriever
|
||||||
|
|
||||||
|
response = client.get("/api/rag/resources?query=test")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.json()["resources"]) == 1
|
||||||
|
|
||||||
|
@patch("src.server.app.build_retriever")
|
||||||
|
def test_rag_resources_without_retriever(self, mock_build_retriever, client):
|
||||||
|
mock_build_retriever.return_value = None
|
||||||
|
|
||||||
|
response = client.get("/api/rag/resources")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["resources"] == []
|
||||||
|
|
||||||
|
|
||||||
|
class TestChatStreamEndpoint:
|
||||||
|
@patch("src.server.app.graph")
|
||||||
|
def test_chat_stream_with_default_thread_id(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": {},
|
||||||
|
"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:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("src.server.app.graph")
|
||||||
|
async def test_astream_workflow_generator_basic_flow(self, mock_graph):
|
||||||
|
# Mock AI message chunk
|
||||||
|
mock_message = AIMessageChunk(content="Hello world")
|
||||||
|
mock_message.id = "msg_123"
|
||||||
|
mock_message.response_metadata = {}
|
||||||
|
mock_message.tool_calls = []
|
||||||
|
mock_message.tool_call_chunks = []
|
||||||
|
|
||||||
|
# Mock the async stream - yield messages in the correct format
|
||||||
|
async def mock_astream(*args, **kwargs):
|
||||||
|
# Yield a tuple (message, metadata) instead of just [message]
|
||||||
|
yield ("agent1:subagent", "messages", (mock_message, {}))
|
||||||
|
|
||||||
|
mock_graph.astream = mock_astream
|
||||||
|
|
||||||
|
messages = [{"role": "user", "content": "Hello"}]
|
||||||
|
thread_id = "test_thread"
|
||||||
|
resources = []
|
||||||
|
|
||||||
|
generator = _astream_workflow_generator(
|
||||||
|
messages=messages,
|
||||||
|
thread_id=thread_id,
|
||||||
|
resources=resources,
|
||||||
|
max_plan_iterations=3,
|
||||||
|
max_step_num=10,
|
||||||
|
max_search_results=5,
|
||||||
|
auto_accepted_plan=True,
|
||||||
|
interrupt_feedback="",
|
||||||
|
mcp_settings={},
|
||||||
|
enable_background_investigation=False,
|
||||||
|
report_style=ReportStyle.ACADEMIC,
|
||||||
|
enable_deep_thinking=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
events = []
|
||||||
|
async for event in generator:
|
||||||
|
events.append(event)
|
||||||
|
|
||||||
|
assert len(events) == 1
|
||||||
|
assert "event: message_chunk" in events[0]
|
||||||
|
assert "Hello world" in events[0]
|
||||||
|
# Check for the actual agent name that appears in the output
|
||||||
|
assert '"agent": "a"' in events[0]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("src.server.app.graph")
|
||||||
|
async def test_astream_workflow_generator_with_interrupt_feedback(self, mock_graph):
|
||||||
|
|
||||||
|
# Mock the async stream
|
||||||
|
async def mock_astream(*args, **kwargs):
|
||||||
|
# Verify that Command is passed as input when interrupt_feedback is provided
|
||||||
|
assert isinstance(args[0], Command)
|
||||||
|
assert "[edit_plan] Hello" in args[0].resume
|
||||||
|
yield ("agent1", "step1", {"test": "data"})
|
||||||
|
|
||||||
|
mock_graph.astream = mock_astream
|
||||||
|
|
||||||
|
messages = [{"role": "user", "content": "Hello"}]
|
||||||
|
|
||||||
|
generator = _astream_workflow_generator(
|
||||||
|
messages=messages,
|
||||||
|
thread_id="test_thread",
|
||||||
|
resources=[],
|
||||||
|
max_plan_iterations=3,
|
||||||
|
max_step_num=10,
|
||||||
|
max_search_results=5,
|
||||||
|
auto_accepted_plan=False,
|
||||||
|
interrupt_feedback="edit_plan",
|
||||||
|
mcp_settings={},
|
||||||
|
enable_background_investigation=False,
|
||||||
|
report_style=ReportStyle.ACADEMIC,
|
||||||
|
enable_deep_thinking=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
events = []
|
||||||
|
async for event in generator:
|
||||||
|
events.append(event)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("src.server.app.graph")
|
||||||
|
async def test_astream_workflow_generator_interrupt_event(self, mock_graph):
|
||||||
|
# Mock interrupt data
|
||||||
|
mock_interrupt = MagicMock()
|
||||||
|
mock_interrupt.ns = ["interrupt_id"]
|
||||||
|
mock_interrupt.value = "Plan requires approval"
|
||||||
|
|
||||||
|
interrupt_data = {"__interrupt__": [mock_interrupt]}
|
||||||
|
|
||||||
|
async def mock_astream(*args, **kwargs):
|
||||||
|
yield ("agent1", "step1", interrupt_data)
|
||||||
|
|
||||||
|
mock_graph.astream = mock_astream
|
||||||
|
|
||||||
|
generator = _astream_workflow_generator(
|
||||||
|
messages=[],
|
||||||
|
thread_id="test_thread",
|
||||||
|
resources=[],
|
||||||
|
max_plan_iterations=3,
|
||||||
|
max_step_num=10,
|
||||||
|
max_search_results=5,
|
||||||
|
auto_accepted_plan=True,
|
||||||
|
interrupt_feedback="",
|
||||||
|
mcp_settings={},
|
||||||
|
enable_background_investigation=False,
|
||||||
|
report_style=ReportStyle.ACADEMIC,
|
||||||
|
enable_deep_thinking=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
events = []
|
||||||
|
async for event in generator:
|
||||||
|
events.append(event)
|
||||||
|
|
||||||
|
assert len(events) == 1
|
||||||
|
assert "event: interrupt" in events[0]
|
||||||
|
assert "Plan requires approval" in events[0]
|
||||||
|
assert "interrupt_id" in events[0]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("src.server.app.graph")
|
||||||
|
async def test_astream_workflow_generator_tool_message(self, mock_graph):
|
||||||
|
|
||||||
|
# Mock tool message
|
||||||
|
mock_tool_message = ToolMessage(content="Tool result", tool_call_id="tool_123")
|
||||||
|
mock_tool_message.id = "msg_456"
|
||||||
|
|
||||||
|
async def mock_astream(*args, **kwargs):
|
||||||
|
yield ("agent1:subagent", "step1", (mock_tool_message, {}))
|
||||||
|
|
||||||
|
mock_graph.astream = mock_astream
|
||||||
|
|
||||||
|
generator = _astream_workflow_generator(
|
||||||
|
messages=[],
|
||||||
|
thread_id="test_thread",
|
||||||
|
resources=[],
|
||||||
|
max_plan_iterations=3,
|
||||||
|
max_step_num=10,
|
||||||
|
max_search_results=5,
|
||||||
|
auto_accepted_plan=True,
|
||||||
|
interrupt_feedback="",
|
||||||
|
mcp_settings={},
|
||||||
|
enable_background_investigation=False,
|
||||||
|
report_style=ReportStyle.ACADEMIC,
|
||||||
|
enable_deep_thinking=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
events = []
|
||||||
|
async for event in generator:
|
||||||
|
events.append(event)
|
||||||
|
|
||||||
|
assert len(events) == 1
|
||||||
|
assert "event: tool_call_result" in events[0]
|
||||||
|
assert "Tool result" in events[0]
|
||||||
|
assert "tool_123" in events[0]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("src.server.app.graph")
|
||||||
|
async def test_astream_workflow_generator_ai_message_with_tool_calls(
|
||||||
|
self, mock_graph
|
||||||
|
):
|
||||||
|
|
||||||
|
# Mock AI message with tool calls
|
||||||
|
mock_ai_message = AIMessageChunk(content="Making tool call")
|
||||||
|
mock_ai_message.id = "msg_789"
|
||||||
|
mock_ai_message.response_metadata = {"finish_reason": "tool_calls"}
|
||||||
|
mock_ai_message.tool_calls = [{"name": "search", "args": {"query": "test"}}]
|
||||||
|
mock_ai_message.tool_call_chunks = [{"name": "search"}]
|
||||||
|
|
||||||
|
async def mock_astream(*args, **kwargs):
|
||||||
|
yield ("agent1:subagent", "step1", (mock_ai_message, {}))
|
||||||
|
|
||||||
|
mock_graph.astream = mock_astream
|
||||||
|
|
||||||
|
generator = _astream_workflow_generator(
|
||||||
|
messages=[],
|
||||||
|
thread_id="test_thread",
|
||||||
|
resources=[],
|
||||||
|
max_plan_iterations=3,
|
||||||
|
max_step_num=10,
|
||||||
|
max_search_results=5,
|
||||||
|
auto_accepted_plan=True,
|
||||||
|
interrupt_feedback="",
|
||||||
|
mcp_settings={},
|
||||||
|
enable_background_investigation=False,
|
||||||
|
report_style=ReportStyle.ACADEMIC,
|
||||||
|
enable_deep_thinking=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
events = []
|
||||||
|
async for event in generator:
|
||||||
|
events.append(event)
|
||||||
|
|
||||||
|
assert len(events) == 1
|
||||||
|
assert "event: tool_calls" in events[0]
|
||||||
|
assert "Making tool call" in events[0]
|
||||||
|
assert "tool_calls" in events[0]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("src.server.app.graph")
|
||||||
|
async def test_astream_workflow_generator_ai_message_with_tool_call_chunks(
|
||||||
|
self, mock_graph
|
||||||
|
):
|
||||||
|
|
||||||
|
# Mock AI message with only tool call chunks
|
||||||
|
mock_ai_message = AIMessageChunk(content="Streaming tool call")
|
||||||
|
mock_ai_message.id = "msg_101"
|
||||||
|
mock_ai_message.response_metadata = {}
|
||||||
|
mock_ai_message.tool_calls = []
|
||||||
|
mock_ai_message.tool_call_chunks = [{"name": "search", "index": 0}]
|
||||||
|
|
||||||
|
async def mock_astream(*args, **kwargs):
|
||||||
|
yield ("agent1:subagent", "step1", (mock_ai_message, {}))
|
||||||
|
|
||||||
|
mock_graph.astream = mock_astream
|
||||||
|
|
||||||
|
generator = _astream_workflow_generator(
|
||||||
|
messages=[],
|
||||||
|
thread_id="test_thread",
|
||||||
|
resources=[],
|
||||||
|
max_plan_iterations=3,
|
||||||
|
max_step_num=10,
|
||||||
|
max_search_results=5,
|
||||||
|
auto_accepted_plan=True,
|
||||||
|
interrupt_feedback="",
|
||||||
|
mcp_settings={},
|
||||||
|
enable_background_investigation=False,
|
||||||
|
report_style=ReportStyle.ACADEMIC,
|
||||||
|
enable_deep_thinking=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
events = []
|
||||||
|
async for event in generator:
|
||||||
|
events.append(event)
|
||||||
|
|
||||||
|
assert len(events) == 1
|
||||||
|
assert "event: tool_call_chunks" in events[0]
|
||||||
|
assert "Streaming tool call" in events[0]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("src.server.app.graph")
|
||||||
|
async def test_astream_workflow_generator_with_finish_reason(self, mock_graph):
|
||||||
|
|
||||||
|
# Mock AI message with finish reason
|
||||||
|
mock_ai_message = AIMessageChunk(content="Complete response")
|
||||||
|
mock_ai_message.id = "msg_finish"
|
||||||
|
mock_ai_message.response_metadata = {"finish_reason": "stop"}
|
||||||
|
mock_ai_message.tool_calls = []
|
||||||
|
mock_ai_message.tool_call_chunks = []
|
||||||
|
|
||||||
|
async def mock_astream(*args, **kwargs):
|
||||||
|
yield ("agent1:subagent", "step1", (mock_ai_message, {}))
|
||||||
|
|
||||||
|
mock_graph.astream = mock_astream
|
||||||
|
|
||||||
|
generator = _astream_workflow_generator(
|
||||||
|
messages=[],
|
||||||
|
thread_id="test_thread",
|
||||||
|
resources=[],
|
||||||
|
max_plan_iterations=3,
|
||||||
|
max_step_num=10,
|
||||||
|
max_search_results=5,
|
||||||
|
auto_accepted_plan=True,
|
||||||
|
interrupt_feedback="",
|
||||||
|
mcp_settings={},
|
||||||
|
enable_background_investigation=False,
|
||||||
|
report_style=ReportStyle.ACADEMIC,
|
||||||
|
enable_deep_thinking=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
events = []
|
||||||
|
async for event in generator:
|
||||||
|
events.append(event)
|
||||||
|
|
||||||
|
assert len(events) == 1
|
||||||
|
assert "event: message_chunk" in events[0]
|
||||||
|
assert "finish_reason" in events[0]
|
||||||
|
assert "stop" in events[0]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("src.server.app.graph")
|
||||||
|
async def test_astream_workflow_generator_config_passed_correctly(self, mock_graph):
|
||||||
|
|
||||||
|
mock_ai_message = AIMessageChunk(content="Test")
|
||||||
|
mock_ai_message.id = "test_id"
|
||||||
|
mock_ai_message.response_metadata = {}
|
||||||
|
mock_ai_message.tool_calls = []
|
||||||
|
mock_ai_message.tool_call_chunks = []
|
||||||
|
|
||||||
|
async def verify_config(*args, **kwargs):
|
||||||
|
config = kwargs.get("config", {})
|
||||||
|
assert config["thread_id"] == "test_thread"
|
||||||
|
assert config["max_plan_iterations"] == 5
|
||||||
|
assert config["max_step_num"] == 20
|
||||||
|
assert config["max_search_results"] == 10
|
||||||
|
assert config["report_style"] == ReportStyle.NEWS.value
|
||||||
|
yield ("agent1", "messages", [mock_ai_message])
|
||||||
|
|
||||||
|
|
||||||
|
class TestGenerateProseEndpoint:
|
||||||
|
@patch("src.server.app.build_prose_graph")
|
||||||
|
def test_generate_prose_success(self, mock_build_graph, client):
|
||||||
|
# Mock the workflow and its astream method
|
||||||
|
mock_workflow = MagicMock()
|
||||||
|
mock_build_graph.return_value = mock_workflow
|
||||||
|
|
||||||
|
class MockEvent:
|
||||||
|
def __init__(self, content):
|
||||||
|
self.content = content
|
||||||
|
|
||||||
|
async def mock_astream(*args, **kwargs):
|
||||||
|
yield (None, [MockEvent("Generated prose 1")])
|
||||||
|
yield (None, [MockEvent("Generated prose 2")])
|
||||||
|
|
||||||
|
mock_workflow.astream.return_value = mock_astream()
|
||||||
|
request_data = {
|
||||||
|
"prompt": "Write a story.",
|
||||||
|
"option": "default",
|
||||||
|
"command": "generate",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post("/api/prose/generate", json=request_data)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.headers["content-type"].startswith("text/event-stream")
|
||||||
|
|
||||||
|
# Read the streaming response content
|
||||||
|
content = b"".join(response.iter_bytes())
|
||||||
|
assert b"Generated prose 1" in content or b"Generated prose 2" in content
|
||||||
|
|
||||||
|
@patch("src.server.app.build_prose_graph")
|
||||||
|
def test_generate_prose_error(self, mock_build_graph, client):
|
||||||
|
mock_build_graph.side_effect = Exception("Prose generation failed")
|
||||||
|
request_data = {
|
||||||
|
"prompt": "Write a story.",
|
||||||
|
"option": "default",
|
||||||
|
"command": "generate",
|
||||||
|
}
|
||||||
|
response = client.post("/api/prose/generate", json=request_data)
|
||||||
|
assert response.status_code == 500
|
||||||
|
assert response.json()["detail"] == "Internal Server Error"
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pydantic import ValidationError
|
||||||
|
from src.config.report_style import ReportStyle
|
||||||
|
from src.rag.retriever import Resource
|
||||||
|
from unittest.mock import AsyncMock, patch, MagicMock
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from src.server.chat_request import (
|
||||||
|
ContentItem,
|
||||||
|
ChatMessage,
|
||||||
|
ChatRequest,
|
||||||
|
TTSRequest,
|
||||||
|
GeneratePodcastRequest,
|
||||||
|
GeneratePPTRequest,
|
||||||
|
GenerateProseRequest,
|
||||||
|
EnhancePromptRequest,
|
||||||
|
)
|
||||||
|
import src.server.mcp_utils as mcp_utils # Assuming mcp_utils is the module to test
|
||||||
|
|
||||||
|
|
||||||
|
def test_content_item_text_and_image():
|
||||||
|
item_text = ContentItem(type="text", text="hello")
|
||||||
|
assert item_text.type == "text"
|
||||||
|
assert item_text.text == "hello"
|
||||||
|
assert item_text.image_url is None
|
||||||
|
|
||||||
|
item_image = ContentItem(type="image", image_url="http://img.com/1.png")
|
||||||
|
assert item_image.type == "image"
|
||||||
|
assert item_image.text is None
|
||||||
|
assert item_image.image_url == "http://img.com/1.png"
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_message_with_string_content():
|
||||||
|
msg = ChatMessage(role="user", content="Hello!")
|
||||||
|
assert msg.role == "user"
|
||||||
|
assert msg.content == "Hello!"
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_message_with_content_items():
|
||||||
|
items = [ContentItem(type="text", text="hi")]
|
||||||
|
msg = ChatMessage(role="assistant", content=items)
|
||||||
|
assert msg.role == "assistant"
|
||||||
|
assert isinstance(msg.content, list)
|
||||||
|
assert msg.content[0].type == "text"
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_request_defaults():
|
||||||
|
req = ChatRequest()
|
||||||
|
assert req.messages == []
|
||||||
|
assert req.resources == []
|
||||||
|
assert req.debug is False
|
||||||
|
assert req.thread_id == "__default__"
|
||||||
|
assert req.max_plan_iterations == 1
|
||||||
|
assert req.max_step_num == 3
|
||||||
|
assert req.max_search_results == 3
|
||||||
|
assert req.auto_accepted_plan is False
|
||||||
|
assert req.interrupt_feedback is None
|
||||||
|
assert req.mcp_settings is None
|
||||||
|
assert req.enable_background_investigation is True
|
||||||
|
assert req.report_style == ReportStyle.ACADEMIC
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_request_with_values():
|
||||||
|
resource = Resource(
|
||||||
|
name="test", type="doc", uri="some-uri-value", title="some-title-value"
|
||||||
|
)
|
||||||
|
msg = ChatMessage(role="user", content="hi")
|
||||||
|
req = ChatRequest(
|
||||||
|
messages=[msg],
|
||||||
|
resources=[resource],
|
||||||
|
debug=True,
|
||||||
|
thread_id="tid",
|
||||||
|
max_plan_iterations=2,
|
||||||
|
max_step_num=5,
|
||||||
|
max_search_results=10,
|
||||||
|
auto_accepted_plan=True,
|
||||||
|
interrupt_feedback="stop",
|
||||||
|
mcp_settings={"foo": "bar"},
|
||||||
|
enable_background_investigation=False,
|
||||||
|
report_style="academic",
|
||||||
|
)
|
||||||
|
assert req.messages[0].role == "user"
|
||||||
|
assert req.debug is True
|
||||||
|
assert req.thread_id == "tid"
|
||||||
|
assert req.max_plan_iterations == 2
|
||||||
|
assert req.max_step_num == 5
|
||||||
|
assert req.max_search_results == 10
|
||||||
|
assert req.auto_accepted_plan is True
|
||||||
|
assert req.interrupt_feedback == "stop"
|
||||||
|
assert req.mcp_settings == {"foo": "bar"}
|
||||||
|
assert req.enable_background_investigation is False
|
||||||
|
assert req.report_style == ReportStyle.ACADEMIC
|
||||||
|
|
||||||
|
|
||||||
|
def test_tts_request_defaults():
|
||||||
|
req = TTSRequest(text="hello")
|
||||||
|
assert req.text == "hello"
|
||||||
|
assert req.voice_type == "BV700_V2_streaming"
|
||||||
|
assert req.encoding == "mp3"
|
||||||
|
assert req.speed_ratio == 1.0
|
||||||
|
assert req.volume_ratio == 1.0
|
||||||
|
assert req.pitch_ratio == 1.0
|
||||||
|
assert req.text_type == "plain"
|
||||||
|
assert req.with_frontend == 1
|
||||||
|
assert req.frontend_type == "unitTson"
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_podcast_request():
|
||||||
|
req = GeneratePodcastRequest(content="Podcast content")
|
||||||
|
assert req.content == "Podcast content"
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_ppt_request():
|
||||||
|
req = GeneratePPTRequest(content="PPT content")
|
||||||
|
assert req.content == "PPT content"
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_prose_request():
|
||||||
|
req = GenerateProseRequest(prompt="Write a poem", option="poet", command="rhyme")
|
||||||
|
assert req.prompt == "Write a poem"
|
||||||
|
assert req.option == "poet"
|
||||||
|
assert req.command == "rhyme"
|
||||||
|
|
||||||
|
req2 = GenerateProseRequest(prompt="Write", option="short")
|
||||||
|
assert req2.command == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_enhance_prompt_request_defaults():
|
||||||
|
req = EnhancePromptRequest(prompt="Improve this")
|
||||||
|
assert req.prompt == "Improve this"
|
||||||
|
assert req.context == ""
|
||||||
|
assert req.report_style == "academic"
|
||||||
|
|
||||||
|
|
||||||
|
def test_content_item_validation_error():
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
ContentItem() # missing required 'type'
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_message_validation_error():
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
ChatMessage(role="user") # missing content
|
||||||
|
|
||||||
|
|
||||||
|
def test_tts_request_validation_error():
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
TTSRequest() # missing required 'text'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("src.server.mcp_utils._get_tools_from_client_session", new_callable=AsyncMock)
|
||||||
|
@patch("src.server.mcp_utils.StdioServerParameters")
|
||||||
|
@patch("src.server.mcp_utils.stdio_client")
|
||||||
|
async def test_load_mcp_tools_exception_handling(
|
||||||
|
mock_stdio_client, mock_StdioServerParameters, mock_get_tools
|
||||||
|
): # Changed to async def
|
||||||
|
mock_get_tools.side_effect = Exception("unexpected error")
|
||||||
|
mock_StdioServerParameters.return_value = MagicMock()
|
||||||
|
mock_stdio_client.return_value = MagicMock()
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
await mcp_utils.load_mcp_tools(server_type="stdio", command="foo") # Use await
|
||||||
|
assert exc.value.status_code == 500
|
||||||
|
assert "unexpected error" in exc.value.detail
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pydantic import ValidationError
|
||||||
|
from src.server.mcp_request import MCPServerMetadataRequest, MCPServerMetadataResponse
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcp_server_metadata_request_required_fields():
|
||||||
|
# 'transport' is required
|
||||||
|
req = MCPServerMetadataRequest(transport="stdio")
|
||||||
|
assert req.transport == "stdio"
|
||||||
|
assert req.command is None
|
||||||
|
assert req.args is None
|
||||||
|
assert req.url is None
|
||||||
|
assert req.env is None
|
||||||
|
assert req.timeout_seconds is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcp_server_metadata_request_optional_fields():
|
||||||
|
req = MCPServerMetadataRequest(
|
||||||
|
transport="sse",
|
||||||
|
command="run",
|
||||||
|
args=["--foo", "bar"],
|
||||||
|
url="http://localhost:8080",
|
||||||
|
env={"FOO": "BAR"},
|
||||||
|
timeout_seconds=30,
|
||||||
|
)
|
||||||
|
assert req.transport == "sse"
|
||||||
|
assert req.command == "run"
|
||||||
|
assert req.args == ["--foo", "bar"]
|
||||||
|
assert req.url == "http://localhost:8080"
|
||||||
|
assert req.env == {"FOO": "BAR"}
|
||||||
|
assert req.timeout_seconds == 30
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcp_server_metadata_request_missing_transport():
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
MCPServerMetadataRequest()
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcp_server_metadata_response_required_fields():
|
||||||
|
resp = MCPServerMetadataResponse(transport="stdio")
|
||||||
|
assert resp.transport == "stdio"
|
||||||
|
assert resp.command is None
|
||||||
|
assert resp.args is None
|
||||||
|
assert resp.url is None
|
||||||
|
assert resp.env is None
|
||||||
|
assert resp.tools == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcp_server_metadata_response_optional_fields():
|
||||||
|
resp = MCPServerMetadataResponse(
|
||||||
|
transport="sse",
|
||||||
|
command="run",
|
||||||
|
args=["--foo", "bar"],
|
||||||
|
url="http://localhost:8080",
|
||||||
|
env={"FOO": "BAR"},
|
||||||
|
tools=["tool1", "tool2"],
|
||||||
|
)
|
||||||
|
assert resp.transport == "sse"
|
||||||
|
assert resp.command == "run"
|
||||||
|
assert resp.args == ["--foo", "bar"]
|
||||||
|
assert resp.url == "http://localhost:8080"
|
||||||
|
assert resp.env == {"FOO": "BAR"}
|
||||||
|
assert resp.tools == ["tool1", "tool2"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcp_server_metadata_response_tools_default_factory():
|
||||||
|
resp1 = MCPServerMetadataResponse(transport="stdio")
|
||||||
|
resp2 = MCPServerMetadataResponse(transport="stdio")
|
||||||
|
resp1.tools.append("toolA")
|
||||||
|
assert resp2.tools == [] # Should not share list between instances
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, patch, MagicMock
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
import src.server.mcp_utils as mcp_utils
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("src.server.mcp_utils.ClientSession")
|
||||||
|
async def test__get_tools_from_client_session_success(mock_ClientSession):
|
||||||
|
mock_read = AsyncMock()
|
||||||
|
mock_write = AsyncMock()
|
||||||
|
mock_context_manager = AsyncMock()
|
||||||
|
mock_context_manager.__aenter__.return_value = (mock_read, mock_write)
|
||||||
|
mock_context_manager.__aexit__.return_value = None
|
||||||
|
|
||||||
|
mock_session = AsyncMock()
|
||||||
|
mock_session.__aenter__.return_value = mock_session
|
||||||
|
mock_session.__aexit__.return_value = None
|
||||||
|
mock_session.initialize = AsyncMock()
|
||||||
|
mock_tools_obj = MagicMock()
|
||||||
|
mock_tools_obj.tools = ["tool1", "tool2"]
|
||||||
|
mock_session.list_tools = AsyncMock(return_value=mock_tools_obj)
|
||||||
|
mock_ClientSession.return_value = mock_session
|
||||||
|
|
||||||
|
result = await mcp_utils._get_tools_from_client_session(
|
||||||
|
mock_context_manager, timeout_seconds=5
|
||||||
|
)
|
||||||
|
assert result == ["tool1", "tool2"]
|
||||||
|
mock_session.initialize.assert_awaited_once()
|
||||||
|
mock_session.list_tools.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("src.server.mcp_utils._get_tools_from_client_session", new_callable=AsyncMock)
|
||||||
|
@patch("src.server.mcp_utils.StdioServerParameters")
|
||||||
|
@patch("src.server.mcp_utils.stdio_client")
|
||||||
|
async def test_load_mcp_tools_stdio_success(
|
||||||
|
mock_stdio_client, mock_StdioServerParameters, mock_get_tools
|
||||||
|
):
|
||||||
|
mock_get_tools.return_value = ["toolA"]
|
||||||
|
params = MagicMock()
|
||||||
|
mock_StdioServerParameters.return_value = params
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_stdio_client.return_value = mock_client
|
||||||
|
|
||||||
|
result = await mcp_utils.load_mcp_tools(
|
||||||
|
server_type="stdio",
|
||||||
|
command="echo",
|
||||||
|
args=["foo"],
|
||||||
|
env={"FOO": "BAR"},
|
||||||
|
timeout_seconds=3,
|
||||||
|
)
|
||||||
|
assert result == ["toolA"]
|
||||||
|
mock_StdioServerParameters.assert_called_once_with(
|
||||||
|
command="echo", args=["foo"], env={"FOO": "BAR"}
|
||||||
|
)
|
||||||
|
mock_stdio_client.assert_called_once_with(params)
|
||||||
|
mock_get_tools.assert_awaited_once_with(mock_client, 3)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_load_mcp_tools_stdio_missing_command():
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
await mcp_utils.load_mcp_tools(server_type="stdio")
|
||||||
|
assert exc.value.status_code == 400
|
||||||
|
assert "Command is required" in exc.value.detail
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("src.server.mcp_utils._get_tools_from_client_session", new_callable=AsyncMock)
|
||||||
|
@patch("src.server.mcp_utils.sse_client")
|
||||||
|
async def test_load_mcp_tools_sse_success(mock_sse_client, mock_get_tools):
|
||||||
|
mock_get_tools.return_value = ["toolB"]
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_sse_client.return_value = mock_client
|
||||||
|
|
||||||
|
result = await mcp_utils.load_mcp_tools(
|
||||||
|
server_type="sse",
|
||||||
|
url="http://localhost:1234",
|
||||||
|
timeout_seconds=7,
|
||||||
|
)
|
||||||
|
assert result == ["toolB"]
|
||||||
|
mock_sse_client.assert_called_once_with(url="http://localhost:1234")
|
||||||
|
mock_get_tools.assert_awaited_once_with(mock_client, 7)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_load_mcp_tools_sse_missing_url():
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
await mcp_utils.load_mcp_tools(server_type="sse")
|
||||||
|
assert exc.value.status_code == 400
|
||||||
|
assert "URL is required" in exc.value.detail
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_load_mcp_tools_unsupported_type():
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
await mcp_utils.load_mcp_tools(server_type="unknown")
|
||||||
|
assert exc.value.status_code == 400
|
||||||
|
assert "Unsupported server type" in exc.value.detail
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("src.server.mcp_utils._get_tools_from_client_session", new_callable=AsyncMock)
|
||||||
|
@patch("src.server.mcp_utils.StdioServerParameters")
|
||||||
|
@patch("src.server.mcp_utils.stdio_client")
|
||||||
|
async def test_load_mcp_tools_exception_handling(
|
||||||
|
mock_stdio_client, mock_StdioServerParameters, mock_get_tools
|
||||||
|
):
|
||||||
|
mock_get_tools.side_effect = Exception("unexpected error")
|
||||||
|
mock_StdioServerParameters.return_value = MagicMock()
|
||||||
|
mock_stdio_client.return_value = MagicMock()
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
await mcp_utils.load_mcp_tools(server_type="stdio", command="foo")
|
||||||
|
assert exc.value.status_code == 500
|
||||||
|
assert "unexpected error" in exc.value.detail
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
from unittest.mock import Mock, patch
|
||||||
|
from src.tools.crawl import crawl_tool
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrawlTool:
|
||||||
|
|
||||||
|
@patch("src.tools.crawl.Crawler")
|
||||||
|
def test_crawl_tool_success(self, mock_crawler_class):
|
||||||
|
# Arrange
|
||||||
|
mock_crawler = Mock()
|
||||||
|
mock_article = Mock()
|
||||||
|
mock_article.to_markdown.return_value = (
|
||||||
|
"# Test Article\nThis is test content." * 100
|
||||||
|
)
|
||||||
|
mock_crawler.crawl.return_value = mock_article
|
||||||
|
mock_crawler_class.return_value = mock_crawler
|
||||||
|
|
||||||
|
url = "https://example.com"
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = crawl_tool(url)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
assert result["url"] == url
|
||||||
|
assert "crawled_content" in result
|
||||||
|
assert len(result["crawled_content"]) <= 1000
|
||||||
|
mock_crawler_class.assert_called_once()
|
||||||
|
mock_crawler.crawl.assert_called_once_with(url)
|
||||||
|
mock_article.to_markdown.assert_called_once()
|
||||||
|
|
||||||
|
@patch("src.tools.crawl.Crawler")
|
||||||
|
def test_crawl_tool_short_content(self, mock_crawler_class):
|
||||||
|
# Arrange
|
||||||
|
mock_crawler = Mock()
|
||||||
|
mock_article = Mock()
|
||||||
|
short_content = "Short content"
|
||||||
|
mock_article.to_markdown.return_value = short_content
|
||||||
|
mock_crawler.crawl.return_value = mock_article
|
||||||
|
mock_crawler_class.return_value = mock_crawler
|
||||||
|
|
||||||
|
url = "https://example.com"
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = crawl_tool(url)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result["crawled_content"] == short_content
|
||||||
|
|
||||||
|
@patch("src.tools.crawl.Crawler")
|
||||||
|
@patch("src.tools.crawl.logger")
|
||||||
|
def test_crawl_tool_crawler_exception(self, mock_logger, mock_crawler_class):
|
||||||
|
# Arrange
|
||||||
|
mock_crawler = Mock()
|
||||||
|
mock_crawler.crawl.side_effect = Exception("Network error")
|
||||||
|
mock_crawler_class.return_value = mock_crawler
|
||||||
|
|
||||||
|
url = "https://example.com"
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = crawl_tool(url)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert isinstance(result, str)
|
||||||
|
assert "Failed to crawl" in result
|
||||||
|
assert "Network error" in result
|
||||||
|
mock_logger.error.assert_called_once()
|
||||||
|
|
||||||
|
@patch("src.tools.crawl.Crawler")
|
||||||
|
@patch("src.tools.crawl.logger")
|
||||||
|
def test_crawl_tool_crawler_instantiation_exception(
|
||||||
|
self, mock_logger, mock_crawler_class
|
||||||
|
):
|
||||||
|
# Arrange
|
||||||
|
mock_crawler_class.side_effect = Exception("Crawler init error")
|
||||||
|
|
||||||
|
url = "https://example.com"
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = crawl_tool(url)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert isinstance(result, str)
|
||||||
|
assert "Failed to crawl" in result
|
||||||
|
assert "Crawler init error" in result
|
||||||
|
mock_logger.error.assert_called_once()
|
||||||
|
|
||||||
|
@patch("src.tools.crawl.Crawler")
|
||||||
|
@patch("src.tools.crawl.logger")
|
||||||
|
def test_crawl_tool_markdown_conversion_exception(
|
||||||
|
self, mock_logger, mock_crawler_class
|
||||||
|
):
|
||||||
|
# Arrange
|
||||||
|
mock_crawler = Mock()
|
||||||
|
mock_article = Mock()
|
||||||
|
mock_article.to_markdown.side_effect = Exception("Markdown conversion error")
|
||||||
|
mock_crawler.crawl.return_value = mock_article
|
||||||
|
mock_crawler_class.return_value = mock_crawler
|
||||||
|
|
||||||
|
url = "https://example.com"
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = crawl_tool(url)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert isinstance(result, str)
|
||||||
|
assert "Failed to crawl" in result
|
||||||
|
assert "Markdown conversion error" in result
|
||||||
|
mock_logger.error.assert_called_once()
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
from unittest.mock import Mock, call, patch
|
||||||
|
from src.tools.decorators import create_logged_tool
|
||||||
|
|
||||||
|
|
||||||
|
class MockBaseTool:
|
||||||
|
"""Mock base tool class for testing."""
|
||||||
|
|
||||||
|
def _run(self, *args, **kwargs):
|
||||||
|
return "base_result"
|
||||||
|
|
||||||
|
|
||||||
|
class TestLoggedToolMixin:
|
||||||
|
|
||||||
|
def test_run_calls_log_operation(self):
|
||||||
|
"""Test that _run calls _log_operation with correct parameters."""
|
||||||
|
# Create a logged tool instance
|
||||||
|
LoggedTool = create_logged_tool(MockBaseTool)
|
||||||
|
tool = LoggedTool()
|
||||||
|
|
||||||
|
# Mock the _log_operation method
|
||||||
|
tool._log_operation = Mock()
|
||||||
|
|
||||||
|
# Call _run with test parameters
|
||||||
|
args = ("arg1", "arg2")
|
||||||
|
kwargs = {"key1": "value1", "key2": "value2"}
|
||||||
|
tool._run(*args, **kwargs)
|
||||||
|
|
||||||
|
# Verify _log_operation was called with correct parameters
|
||||||
|
tool._log_operation.assert_called_once_with("_run", *args, **kwargs)
|
||||||
|
|
||||||
|
def test_run_calls_super_run(self):
|
||||||
|
"""Test that _run calls the parent class _run method."""
|
||||||
|
# Create a logged tool instance
|
||||||
|
LoggedTool = create_logged_tool(MockBaseTool)
|
||||||
|
tool = LoggedTool()
|
||||||
|
|
||||||
|
# Mock the parent _run method
|
||||||
|
with patch.object(
|
||||||
|
MockBaseTool, "_run", return_value="mocked_result"
|
||||||
|
) as mock_super_run:
|
||||||
|
args = ("arg1", "arg2")
|
||||||
|
kwargs = {"key1": "value1"}
|
||||||
|
result = tool._run(*args, **kwargs)
|
||||||
|
|
||||||
|
# Verify super()._run was called with correct parameters
|
||||||
|
mock_super_run.assert_called_once_with(*args, **kwargs)
|
||||||
|
# Verify the result is returned
|
||||||
|
assert result == "mocked_result"
|
||||||
|
|
||||||
|
def test_run_logs_result(self):
|
||||||
|
"""Test that _run logs the result with debug level."""
|
||||||
|
LoggedTool = create_logged_tool(MockBaseTool)
|
||||||
|
tool = LoggedTool()
|
||||||
|
|
||||||
|
with patch("src.tools.decorators.logger.debug") as mock_debug:
|
||||||
|
tool._run("test_arg")
|
||||||
|
|
||||||
|
# Verify debug log was called with correct message
|
||||||
|
mock_debug.assert_has_calls(
|
||||||
|
[
|
||||||
|
call("Tool MockBaseTool._run called with parameters: test_arg"),
|
||||||
|
call("Tool MockBaseTool returned: base_result"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_run_returns_super_result(self):
|
||||||
|
"""Test that _run returns the result from parent class."""
|
||||||
|
LoggedTool = create_logged_tool(MockBaseTool)
|
||||||
|
tool = LoggedTool()
|
||||||
|
|
||||||
|
result = tool._run()
|
||||||
|
assert result == "base_result"
|
||||||
|
|
||||||
|
def test_run_with_no_args(self):
|
||||||
|
"""Test _run method with no arguments."""
|
||||||
|
LoggedTool = create_logged_tool(MockBaseTool)
|
||||||
|
tool = LoggedTool()
|
||||||
|
|
||||||
|
with patch("src.tools.decorators.logger.debug") as mock_debug:
|
||||||
|
tool._log_operation = Mock()
|
||||||
|
|
||||||
|
result = tool._run()
|
||||||
|
|
||||||
|
# Verify _log_operation called with no args
|
||||||
|
tool._log_operation.assert_called_once_with("_run")
|
||||||
|
# Verify result logging
|
||||||
|
mock_debug.assert_called_once()
|
||||||
|
assert result == "base_result"
|
||||||
|
|
||||||
|
def test_run_with_mixed_args_kwargs(self):
|
||||||
|
"""Test _run method with both positional and keyword arguments."""
|
||||||
|
LoggedTool = create_logged_tool(MockBaseTool)
|
||||||
|
tool = LoggedTool()
|
||||||
|
|
||||||
|
tool._log_operation = Mock()
|
||||||
|
|
||||||
|
args = ("pos1", "pos2")
|
||||||
|
kwargs = {"kw1": "val1", "kw2": "val2"}
|
||||||
|
result = tool._run(*args, **kwargs)
|
||||||
|
|
||||||
|
# Verify all arguments passed correctly
|
||||||
|
tool._log_operation.assert_called_once_with("_run", *args, **kwargs)
|
||||||
|
assert result == "base_result"
|
||||||
|
|
||||||
|
def test_run_class_name_replacement(self):
|
||||||
|
"""Test that class name 'Logged' prefix is correctly removed in logging."""
|
||||||
|
LoggedTool = create_logged_tool(MockBaseTool)
|
||||||
|
tool = LoggedTool()
|
||||||
|
|
||||||
|
with patch("src.tools.decorators.logger.debug") as mock_debug:
|
||||||
|
tool._run()
|
||||||
|
|
||||||
|
# Verify the logged class name has 'Logged' prefix removed
|
||||||
|
call_args = mock_debug.call_args[0][0]
|
||||||
|
assert "Tool MockBaseTool returned:" in call_args
|
||||||
|
assert "LoggedMockBaseTool" not in call_args
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch
|
||||||
|
from src.tools.python_repl import python_repl_tool
|
||||||
|
|
||||||
|
|
||||||
|
class TestPythonReplTool:
|
||||||
|
|
||||||
|
@patch("src.tools.python_repl.repl")
|
||||||
|
@patch("src.tools.python_repl.logger")
|
||||||
|
def test_successful_code_execution(self, mock_logger, mock_repl):
|
||||||
|
# Arrange
|
||||||
|
code = "print('Hello, World!')"
|
||||||
|
expected_output = "Hello, World!\n"
|
||||||
|
mock_repl.run.return_value = expected_output
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = python_repl_tool(code)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
mock_repl.run.assert_called_once_with(code)
|
||||||
|
mock_logger.info.assert_called_with("Code execution successful")
|
||||||
|
assert "Successfully executed:" in result
|
||||||
|
assert code in result
|
||||||
|
assert expected_output in result
|
||||||
|
|
||||||
|
@patch("src.tools.python_repl.repl")
|
||||||
|
@patch("src.tools.python_repl.logger")
|
||||||
|
def test_invalid_input_type(self, mock_logger, mock_repl):
|
||||||
|
# Arrange
|
||||||
|
invalid_code = 123
|
||||||
|
|
||||||
|
# Act & Assert - expect ValidationError from LangChain
|
||||||
|
with pytest.raises(Exception) as exc_info:
|
||||||
|
python_repl_tool(invalid_code)
|
||||||
|
|
||||||
|
# Verify that it's a validation error
|
||||||
|
assert "ValidationError" in str(
|
||||||
|
type(exc_info.value)
|
||||||
|
) or "validation error" in str(exc_info.value)
|
||||||
|
|
||||||
|
# The REPL should not be called since validation fails first
|
||||||
|
mock_repl.run.assert_not_called()
|
||||||
|
|
||||||
|
@patch("src.tools.python_repl.repl")
|
||||||
|
@patch("src.tools.python_repl.logger")
|
||||||
|
def test_code_execution_with_error_in_result(self, mock_logger, mock_repl):
|
||||||
|
# Arrange
|
||||||
|
code = "invalid_function()"
|
||||||
|
error_result = "NameError: name 'invalid_function' is not defined"
|
||||||
|
mock_repl.run.return_value = error_result
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = python_repl_tool(code)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
mock_repl.run.assert_called_once_with(code)
|
||||||
|
mock_logger.error.assert_called_with(error_result)
|
||||||
|
assert "Error executing code:" in result
|
||||||
|
assert code in result
|
||||||
|
assert error_result in result
|
||||||
|
|
||||||
|
@patch("src.tools.python_repl.repl")
|
||||||
|
@patch("src.tools.python_repl.logger")
|
||||||
|
def test_code_execution_with_exception_in_result(self, mock_logger, mock_repl):
|
||||||
|
# Arrange
|
||||||
|
code = "1/0"
|
||||||
|
exception_result = "ZeroDivisionError: division by zero"
|
||||||
|
mock_repl.run.return_value = exception_result
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = python_repl_tool(code)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
mock_repl.run.assert_called_once_with(code)
|
||||||
|
mock_logger.error.assert_called_with(exception_result)
|
||||||
|
assert "Error executing code:" in result
|
||||||
|
assert code in result
|
||||||
|
assert exception_result in result
|
||||||
|
|
||||||
|
@patch("src.tools.python_repl.repl")
|
||||||
|
@patch("src.tools.python_repl.logger")
|
||||||
|
def test_code_execution_raises_exception(self, mock_logger, mock_repl):
|
||||||
|
# Arrange
|
||||||
|
code = "print('test')"
|
||||||
|
exception = RuntimeError("REPL failed")
|
||||||
|
mock_repl.run.side_effect = exception
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = python_repl_tool(code)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
mock_repl.run.assert_called_once_with(code)
|
||||||
|
mock_logger.error.assert_called_with(repr(exception))
|
||||||
|
assert "Error executing code:" in result
|
||||||
|
assert code in result
|
||||||
|
assert repr(exception) in result
|
||||||
|
|
||||||
|
@patch("src.tools.python_repl.repl")
|
||||||
|
@patch("src.tools.python_repl.logger")
|
||||||
|
def test_successful_execution_with_calculation(self, mock_logger, mock_repl):
|
||||||
|
# Arrange
|
||||||
|
code = "result = 2 + 3\nprint(result)"
|
||||||
|
expected_output = "5\n"
|
||||||
|
mock_repl.run.return_value = expected_output
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = python_repl_tool(code)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
mock_repl.run.assert_called_once_with(code)
|
||||||
|
mock_logger.info.assert_any_call("Executing Python code")
|
||||||
|
mock_logger.info.assert_any_call("Code execution successful")
|
||||||
|
assert "Successfully executed:" in result
|
||||||
|
assert code in result
|
||||||
|
assert expected_output in result
|
||||||
|
|
||||||
|
@patch("src.tools.python_repl.repl")
|
||||||
|
@patch("src.tools.python_repl.logger")
|
||||||
|
def test_empty_string_code(self, mock_logger, mock_repl):
|
||||||
|
# Arrange
|
||||||
|
code = ""
|
||||||
|
mock_repl.run.return_value = ""
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = python_repl_tool(code)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
mock_repl.run.assert_called_once_with(code)
|
||||||
|
mock_logger.info.assert_called_with("Code execution successful")
|
||||||
|
assert "Successfully executed:" in result
|
||||||
|
|
||||||
|
@patch("src.tools.python_repl.repl")
|
||||||
|
@patch("src.tools.python_repl.logger")
|
||||||
|
def test_logging_calls(self, mock_logger, mock_repl):
|
||||||
|
# Arrange
|
||||||
|
code = "x = 1"
|
||||||
|
mock_repl.run.return_value = ""
|
||||||
|
|
||||||
|
# Act
|
||||||
|
python_repl_tool(code)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
mock_logger.info.assert_any_call("Executing Python code")
|
||||||
|
mock_logger.info.assert_any_call("Code execution successful")
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch
|
||||||
|
from src.tools.search import get_web_search_tool
|
||||||
|
from src.config import SearchEngine
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetWebSearchTool:
|
||||||
|
|
||||||
|
@patch("src.tools.search.SELECTED_SEARCH_ENGINE", SearchEngine.TAVILY.value)
|
||||||
|
def test_get_web_search_tool_tavily(self):
|
||||||
|
tool = get_web_search_tool(max_search_results=5)
|
||||||
|
assert tool.name == "web_search"
|
||||||
|
assert tool.max_results == 5
|
||||||
|
assert tool.include_raw_content is True
|
||||||
|
assert tool.include_images is True
|
||||||
|
assert tool.include_image_descriptions is True
|
||||||
|
|
||||||
|
@patch("src.tools.search.SELECTED_SEARCH_ENGINE", SearchEngine.DUCKDUCKGO.value)
|
||||||
|
def test_get_web_search_tool_duckduckgo(self):
|
||||||
|
tool = get_web_search_tool(max_search_results=3)
|
||||||
|
assert tool.name == "web_search"
|
||||||
|
assert tool.max_results == 3
|
||||||
|
|
||||||
|
@patch("src.tools.search.SELECTED_SEARCH_ENGINE", SearchEngine.BRAVE_SEARCH.value)
|
||||||
|
@patch.dict(os.environ, {"BRAVE_SEARCH_API_KEY": "test_api_key"})
|
||||||
|
def test_get_web_search_tool_brave(self):
|
||||||
|
tool = get_web_search_tool(max_search_results=4)
|
||||||
|
assert tool.name == "web_search"
|
||||||
|
assert tool.search_wrapper.api_key == "test_api_key"
|
||||||
|
|
||||||
|
@patch("src.tools.search.SELECTED_SEARCH_ENGINE", SearchEngine.ARXIV.value)
|
||||||
|
def test_get_web_search_tool_arxiv(self):
|
||||||
|
tool = get_web_search_tool(max_search_results=2)
|
||||||
|
assert tool.name == "web_search"
|
||||||
|
assert tool.api_wrapper.top_k_results == 2
|
||||||
|
assert tool.api_wrapper.load_max_docs == 2
|
||||||
|
assert tool.api_wrapper.load_all_available_meta is True
|
||||||
|
|
||||||
|
@patch("src.tools.search.SELECTED_SEARCH_ENGINE", "unsupported_engine")
|
||||||
|
def test_get_web_search_tool_unsupported_engine(self):
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError, match="Unsupported search engine: unsupported_engine"
|
||||||
|
):
|
||||||
|
get_web_search_tool(max_search_results=1)
|
||||||
|
|
||||||
|
@patch("src.tools.search.SELECTED_SEARCH_ENGINE", SearchEngine.BRAVE_SEARCH.value)
|
||||||
|
@patch.dict(os.environ, {}, clear=True)
|
||||||
|
def test_get_web_search_tool_brave_no_api_key(self):
|
||||||
|
tool = get_web_search_tool(max_search_results=1)
|
||||||
|
assert tool.search_wrapper.api_key == ""
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
import json
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import Mock, patch, AsyncMock, MagicMock
|
||||||
|
import requests
|
||||||
|
from src.tools.tavily_search.tavily_search_api_wrapper import (
|
||||||
|
EnhancedTavilySearchAPIWrapper,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnhancedTavilySearchAPIWrapper:
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def wrapper(self):
|
||||||
|
with patch(
|
||||||
|
"src.tools.tavily_search.tavily_search_api_wrapper.OriginalTavilySearchAPIWrapper"
|
||||||
|
):
|
||||||
|
wrapper = EnhancedTavilySearchAPIWrapper(tavily_api_key="dummy-key")
|
||||||
|
# The parent class is mocked, so initialization won't fail
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_response_data(self):
|
||||||
|
return {
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"title": "Test Title",
|
||||||
|
"url": "https://example.com",
|
||||||
|
"content": "Test content",
|
||||||
|
"score": 0.9,
|
||||||
|
"raw_content": "Raw test content",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"url": "https://example.com/image.jpg",
|
||||||
|
"description": "Test image description",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
@patch("src.tools.tavily_search.tavily_search_api_wrapper.requests.post")
|
||||||
|
def test_raw_results_success(self, mock_post, wrapper, mock_response_data):
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.json.return_value = mock_response_data
|
||||||
|
mock_response.raise_for_status.return_value = None
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
|
||||||
|
result = wrapper.raw_results("test query", max_results=10)
|
||||||
|
|
||||||
|
assert result == mock_response_data
|
||||||
|
mock_post.assert_called_once()
|
||||||
|
call_args = mock_post.call_args
|
||||||
|
assert "json" in call_args.kwargs
|
||||||
|
assert call_args.kwargs["json"]["query"] == "test query"
|
||||||
|
assert call_args.kwargs["json"]["max_results"] == 10
|
||||||
|
|
||||||
|
@patch("src.tools.tavily_search.tavily_search_api_wrapper.requests.post")
|
||||||
|
def test_raw_results_with_all_parameters(
|
||||||
|
self, mock_post, wrapper, mock_response_data
|
||||||
|
):
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.json.return_value = mock_response_data
|
||||||
|
mock_response.raise_for_status.return_value = None
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
|
||||||
|
result = wrapper.raw_results(
|
||||||
|
"test query",
|
||||||
|
max_results=3,
|
||||||
|
search_depth="basic",
|
||||||
|
include_domains=["example.com"],
|
||||||
|
exclude_domains=["spam.com"],
|
||||||
|
include_answer=True,
|
||||||
|
include_raw_content=True,
|
||||||
|
include_images=True,
|
||||||
|
include_image_descriptions=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == mock_response_data
|
||||||
|
call_args = mock_post.call_args
|
||||||
|
params = call_args.kwargs["json"]
|
||||||
|
assert params["include_domains"] == ["example.com"]
|
||||||
|
assert params["exclude_domains"] == ["spam.com"]
|
||||||
|
assert params["include_answer"] is True
|
||||||
|
assert params["include_raw_content"] is True
|
||||||
|
|
||||||
|
@patch("src.tools.tavily_search.tavily_search_api_wrapper.requests.post")
|
||||||
|
def test_raw_results_http_error(self, mock_post, wrapper):
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.raise_for_status.side_effect = requests.HTTPError("API Error")
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
|
||||||
|
with pytest.raises(requests.HTTPError):
|
||||||
|
wrapper.raw_results("test query")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_raw_results_async_success(self, wrapper, mock_response_data):
|
||||||
|
# Create a mock that acts as both the response and its context manager
|
||||||
|
mock_response_cm = AsyncMock()
|
||||||
|
mock_response_cm.__aenter__ = AsyncMock(return_value=mock_response_cm)
|
||||||
|
mock_response_cm.__aexit__ = AsyncMock(return_value=None)
|
||||||
|
mock_response_cm.status = 200
|
||||||
|
mock_response_cm.text = AsyncMock(return_value=json.dumps(mock_response_data))
|
||||||
|
|
||||||
|
# Create mock session that returns the context manager
|
||||||
|
mock_session = AsyncMock()
|
||||||
|
mock_session.post = MagicMock(
|
||||||
|
return_value=mock_response_cm
|
||||||
|
) # Use MagicMock, not AsyncMock
|
||||||
|
|
||||||
|
# Create mock session class
|
||||||
|
mock_session_cm = AsyncMock()
|
||||||
|
mock_session_cm.__aenter__ = AsyncMock(return_value=mock_session)
|
||||||
|
mock_session_cm.__aexit__ = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"src.tools.tavily_search.tavily_search_api_wrapper.aiohttp.ClientSession",
|
||||||
|
return_value=mock_session_cm,
|
||||||
|
):
|
||||||
|
result = await wrapper.raw_results_async("test query")
|
||||||
|
|
||||||
|
assert result == mock_response_data
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_raw_results_async_error(self, wrapper):
|
||||||
|
# Create a mock that acts as both the response and its context manager
|
||||||
|
mock_response_cm = AsyncMock()
|
||||||
|
mock_response_cm.__aenter__ = AsyncMock(return_value=mock_response_cm)
|
||||||
|
mock_response_cm.__aexit__ = AsyncMock(return_value=None)
|
||||||
|
mock_response_cm.status = 400
|
||||||
|
mock_response_cm.reason = "Bad Request"
|
||||||
|
|
||||||
|
# Create mock session that returns the context manager
|
||||||
|
mock_session = AsyncMock()
|
||||||
|
mock_session.post = MagicMock(
|
||||||
|
return_value=mock_response_cm
|
||||||
|
) # Use MagicMock, not AsyncMock
|
||||||
|
|
||||||
|
# Create mock session class
|
||||||
|
mock_session_cm = AsyncMock()
|
||||||
|
mock_session_cm.__aenter__ = AsyncMock(return_value=mock_session)
|
||||||
|
mock_session_cm.__aexit__ = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"src.tools.tavily_search.tavily_search_api_wrapper.aiohttp.ClientSession",
|
||||||
|
return_value=mock_session_cm,
|
||||||
|
):
|
||||||
|
with pytest.raises(Exception, match="Error 400: Bad Request"):
|
||||||
|
await wrapper.raw_results_async("test query")
|
||||||
|
|
||||||
|
def test_clean_results_with_images(self, wrapper, mock_response_data):
|
||||||
|
result = wrapper.clean_results_with_images(mock_response_data)
|
||||||
|
|
||||||
|
assert len(result) == 2
|
||||||
|
|
||||||
|
# Test page result
|
||||||
|
page_result = result[0]
|
||||||
|
assert page_result["type"] == "page"
|
||||||
|
assert page_result["title"] == "Test Title"
|
||||||
|
assert page_result["url"] == "https://example.com"
|
||||||
|
assert page_result["content"] == "Test content"
|
||||||
|
assert page_result["score"] == 0.9
|
||||||
|
assert page_result["raw_content"] == "Raw test content"
|
||||||
|
|
||||||
|
# Test image result
|
||||||
|
image_result = result[1]
|
||||||
|
assert image_result["type"] == "image"
|
||||||
|
assert image_result["image_url"] == "https://example.com/image.jpg"
|
||||||
|
assert image_result["image_description"] == "Test image description"
|
||||||
|
|
||||||
|
def test_clean_results_without_raw_content(self, wrapper):
|
||||||
|
data = {
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"title": "Test Title",
|
||||||
|
"url": "https://example.com",
|
||||||
|
"content": "Test content",
|
||||||
|
"score": 0.9,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"images": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
result = wrapper.clean_results_with_images(data)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
assert "raw_content" not in result[0]
|
||||||
|
|
||||||
|
def test_clean_results_empty_images(self, wrapper):
|
||||||
|
data = {
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"title": "Test Title",
|
||||||
|
"url": "https://example.com",
|
||||||
|
"content": "Test content",
|
||||||
|
"score": 0.9,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"images": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
result = wrapper.clean_results_with_images(data)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0]["type"] == "page"
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import json
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import Mock, patch, AsyncMock
|
||||||
|
from src.tools.tavily_search.tavily_search_results_with_images import (
|
||||||
|
TavilySearchResultsWithImages,
|
||||||
|
)
|
||||||
|
from src.tools.tavily_search.tavily_search_api_wrapper import (
|
||||||
|
EnhancedTavilySearchAPIWrapper,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTavilySearchResultsWithImages:
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_api_wrapper(self):
|
||||||
|
"""Create a mock API wrapper."""
|
||||||
|
wrapper = Mock(spec=EnhancedTavilySearchAPIWrapper)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def search_tool(self, mock_api_wrapper):
|
||||||
|
"""Create a TavilySearchResultsWithImages instance with mocked dependencies."""
|
||||||
|
tool = TavilySearchResultsWithImages(
|
||||||
|
max_results=5,
|
||||||
|
include_answer=True,
|
||||||
|
include_raw_content=True,
|
||||||
|
include_images=True,
|
||||||
|
include_image_descriptions=True,
|
||||||
|
)
|
||||||
|
tool.api_wrapper = mock_api_wrapper
|
||||||
|
return tool
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_raw_results(self):
|
||||||
|
"""Sample raw results from Tavily API."""
|
||||||
|
return {
|
||||||
|
"query": "test query",
|
||||||
|
"answer": "Test answer",
|
||||||
|
"images": ["https://example.com/image1.jpg"],
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"title": "Test Title",
|
||||||
|
"url": "https://example.com",
|
||||||
|
"content": "Test content",
|
||||||
|
"score": 0.95,
|
||||||
|
"raw_content": "Raw test content",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"response_time": 1.5,
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_cleaned_results(self):
|
||||||
|
"""Sample cleaned results."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"title": "Test Title",
|
||||||
|
"url": "https://example.com",
|
||||||
|
"content": "Test content",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_init_default_values(self):
|
||||||
|
"""Test initialization with default values."""
|
||||||
|
tool = TavilySearchResultsWithImages()
|
||||||
|
assert tool.include_image_descriptions is False
|
||||||
|
assert isinstance(tool.api_wrapper, EnhancedTavilySearchAPIWrapper)
|
||||||
|
|
||||||
|
def test_init_custom_values(self):
|
||||||
|
"""Test initialization with custom values."""
|
||||||
|
tool = TavilySearchResultsWithImages(
|
||||||
|
max_results=10, include_image_descriptions=True
|
||||||
|
)
|
||||||
|
assert tool.max_results == 10
|
||||||
|
assert tool.include_image_descriptions is True
|
||||||
|
|
||||||
|
@patch("builtins.print")
|
||||||
|
def test_run_success(
|
||||||
|
self,
|
||||||
|
mock_print,
|
||||||
|
search_tool,
|
||||||
|
mock_api_wrapper,
|
||||||
|
sample_raw_results,
|
||||||
|
sample_cleaned_results,
|
||||||
|
):
|
||||||
|
"""Test successful synchronous run."""
|
||||||
|
mock_api_wrapper.raw_results.return_value = sample_raw_results
|
||||||
|
mock_api_wrapper.clean_results_with_images.return_value = sample_cleaned_results
|
||||||
|
|
||||||
|
result, raw = search_tool._run("test query")
|
||||||
|
|
||||||
|
assert result == sample_cleaned_results
|
||||||
|
assert raw == sample_raw_results
|
||||||
|
|
||||||
|
mock_api_wrapper.raw_results.assert_called_once_with(
|
||||||
|
"test query",
|
||||||
|
search_tool.max_results,
|
||||||
|
search_tool.search_depth,
|
||||||
|
search_tool.include_domains,
|
||||||
|
search_tool.exclude_domains,
|
||||||
|
search_tool.include_answer,
|
||||||
|
search_tool.include_raw_content,
|
||||||
|
search_tool.include_images,
|
||||||
|
search_tool.include_image_descriptions,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_api_wrapper.clean_results_with_images.assert_called_once_with(
|
||||||
|
sample_raw_results
|
||||||
|
)
|
||||||
|
mock_print.assert_called_once()
|
||||||
|
|
||||||
|
@patch("builtins.print")
|
||||||
|
def test_run_exception(self, mock_print, search_tool, mock_api_wrapper):
|
||||||
|
"""Test synchronous run with exception."""
|
||||||
|
mock_api_wrapper.raw_results.side_effect = Exception("API Error")
|
||||||
|
|
||||||
|
result, raw = search_tool._run("test query")
|
||||||
|
|
||||||
|
assert "API Error" in result
|
||||||
|
assert raw == {}
|
||||||
|
mock_api_wrapper.clean_results_with_images.assert_not_called()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("builtins.print")
|
||||||
|
async def test_arun_success(
|
||||||
|
self,
|
||||||
|
mock_print,
|
||||||
|
search_tool,
|
||||||
|
mock_api_wrapper,
|
||||||
|
sample_raw_results,
|
||||||
|
sample_cleaned_results,
|
||||||
|
):
|
||||||
|
"""Test successful asynchronous run."""
|
||||||
|
mock_api_wrapper.raw_results_async = AsyncMock(return_value=sample_raw_results)
|
||||||
|
mock_api_wrapper.clean_results_with_images.return_value = sample_cleaned_results
|
||||||
|
|
||||||
|
result, raw = await search_tool._arun("test query")
|
||||||
|
|
||||||
|
assert result == sample_cleaned_results
|
||||||
|
assert raw == sample_raw_results
|
||||||
|
|
||||||
|
mock_api_wrapper.raw_results_async.assert_called_once_with(
|
||||||
|
"test query",
|
||||||
|
search_tool.max_results,
|
||||||
|
search_tool.search_depth,
|
||||||
|
search_tool.include_domains,
|
||||||
|
search_tool.exclude_domains,
|
||||||
|
search_tool.include_answer,
|
||||||
|
search_tool.include_raw_content,
|
||||||
|
search_tool.include_images,
|
||||||
|
search_tool.include_image_descriptions,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_api_wrapper.clean_results_with_images.assert_called_once_with(
|
||||||
|
sample_raw_results
|
||||||
|
)
|
||||||
|
mock_print.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("builtins.print")
|
||||||
|
async def test_arun_exception(self, mock_print, search_tool, mock_api_wrapper):
|
||||||
|
"""Test asynchronous run with exception."""
|
||||||
|
mock_api_wrapper.raw_results_async = AsyncMock(
|
||||||
|
side_effect=Exception("Async API Error")
|
||||||
|
)
|
||||||
|
|
||||||
|
result, raw = await search_tool._arun("test query")
|
||||||
|
|
||||||
|
assert "Async API Error" in result
|
||||||
|
assert raw == {}
|
||||||
|
mock_api_wrapper.clean_results_with_images.assert_not_called()
|
||||||
|
|
||||||
|
@patch("builtins.print")
|
||||||
|
def test_run_with_run_manager(
|
||||||
|
self,
|
||||||
|
mock_print,
|
||||||
|
search_tool,
|
||||||
|
mock_api_wrapper,
|
||||||
|
sample_raw_results,
|
||||||
|
sample_cleaned_results,
|
||||||
|
):
|
||||||
|
"""Test run with callback manager."""
|
||||||
|
mock_run_manager = Mock()
|
||||||
|
mock_api_wrapper.raw_results.return_value = sample_raw_results
|
||||||
|
mock_api_wrapper.clean_results_with_images.return_value = sample_cleaned_results
|
||||||
|
|
||||||
|
result, raw = search_tool._run("test query", run_manager=mock_run_manager)
|
||||||
|
|
||||||
|
assert result == sample_cleaned_results
|
||||||
|
assert raw == sample_raw_results
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("builtins.print")
|
||||||
|
async def test_arun_with_run_manager(
|
||||||
|
self,
|
||||||
|
mock_print,
|
||||||
|
search_tool,
|
||||||
|
mock_api_wrapper,
|
||||||
|
sample_raw_results,
|
||||||
|
sample_cleaned_results,
|
||||||
|
):
|
||||||
|
"""Test async run with callback manager."""
|
||||||
|
mock_run_manager = Mock()
|
||||||
|
mock_api_wrapper.raw_results_async = AsyncMock(return_value=sample_raw_results)
|
||||||
|
mock_api_wrapper.clean_results_with_images.return_value = sample_cleaned_results
|
||||||
|
|
||||||
|
result, raw = await search_tool._arun(
|
||||||
|
"test query", run_manager=mock_run_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == sample_cleaned_results
|
||||||
|
assert raw == sample_raw_results
|
||||||
|
|
||||||
|
@patch("builtins.print")
|
||||||
|
def test_print_output_format(
|
||||||
|
self,
|
||||||
|
mock_print,
|
||||||
|
search_tool,
|
||||||
|
mock_api_wrapper,
|
||||||
|
sample_raw_results,
|
||||||
|
sample_cleaned_results,
|
||||||
|
):
|
||||||
|
"""Test that print outputs correctly formatted JSON."""
|
||||||
|
mock_api_wrapper.raw_results.return_value = sample_raw_results
|
||||||
|
mock_api_wrapper.clean_results_with_images.return_value = sample_cleaned_results
|
||||||
|
|
||||||
|
search_tool._run("test query")
|
||||||
|
|
||||||
|
# Verify print was called with expected format
|
||||||
|
call_args = mock_print.call_args[0]
|
||||||
|
assert call_args[0] == "sync"
|
||||||
|
assert isinstance(call_args[1], str) # Should be JSON string
|
||||||
|
|
||||||
|
# Verify it's valid JSON
|
||||||
|
json_data = json.loads(call_args[1])
|
||||||
|
assert json_data == sample_cleaned_results
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("builtins.print")
|
||||||
|
async def test_async_print_output_format(
|
||||||
|
self,
|
||||||
|
mock_print,
|
||||||
|
search_tool,
|
||||||
|
mock_api_wrapper,
|
||||||
|
sample_raw_results,
|
||||||
|
sample_cleaned_results,
|
||||||
|
):
|
||||||
|
"""Test that async print outputs correctly formatted JSON."""
|
||||||
|
mock_api_wrapper.raw_results_async = AsyncMock(return_value=sample_raw_results)
|
||||||
|
mock_api_wrapper.clean_results_with_images.return_value = sample_cleaned_results
|
||||||
|
|
||||||
|
await search_tool._arun("test query")
|
||||||
|
|
||||||
|
# Verify print was called with expected format
|
||||||
|
call_args = mock_print.call_args[0]
|
||||||
|
assert call_args[0] == "async"
|
||||||
|
assert isinstance(call_args[1], str) # Should be JSON string
|
||||||
|
|
||||||
|
# Verify it's valid JSON
|
||||||
|
json_data = json.loads(call_args[1])
|
||||||
|
assert json_data == sample_cleaned_results
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
from langchain_core.callbacks import (
|
||||||
|
CallbackManagerForToolRun,
|
||||||
|
AsyncCallbackManagerForToolRun,
|
||||||
|
)
|
||||||
|
import pytest
|
||||||
|
from src.tools.retriever import RetrieverInput, RetrieverTool, get_retriever_tool
|
||||||
|
from src.rag import Document, Retriever, Resource, Chunk
|
||||||
|
|
||||||
|
|
||||||
|
def test_retriever_input_model():
|
||||||
|
input_data = RetrieverInput(keywords="test keywords")
|
||||||
|
assert input_data.keywords == "test keywords"
|
||||||
|
|
||||||
|
|
||||||
|
def test_retriever_tool_init():
|
||||||
|
mock_retriever = Mock(spec=Retriever)
|
||||||
|
resources = [Resource(uri="test://uri", title="Test")]
|
||||||
|
tool = RetrieverTool(retriever=mock_retriever, resources=resources)
|
||||||
|
|
||||||
|
assert tool.name == "local_search_tool"
|
||||||
|
assert "retrieving information" in tool.description
|
||||||
|
assert tool.args_schema == RetrieverInput
|
||||||
|
assert tool.retriever == mock_retriever
|
||||||
|
assert tool.resources == resources
|
||||||
|
|
||||||
|
|
||||||
|
def test_retriever_tool_run_with_results():
|
||||||
|
mock_retriever = Mock(spec=Retriever)
|
||||||
|
chunk = Chunk(content="test content", similarity=0.9)
|
||||||
|
doc = Document(id="doc1", chunks=[chunk])
|
||||||
|
mock_retriever.query_relevant_documents.return_value = [doc]
|
||||||
|
|
||||||
|
resources = [Resource(uri="test://uri", title="Test")]
|
||||||
|
tool = RetrieverTool(retriever=mock_retriever, resources=resources)
|
||||||
|
|
||||||
|
result = tool._run("test keywords")
|
||||||
|
|
||||||
|
mock_retriever.query_relevant_documents.assert_called_once_with(
|
||||||
|
"test keywords", resources
|
||||||
|
)
|
||||||
|
assert isinstance(result, list)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0] == doc.to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
def test_retriever_tool_run_no_results():
|
||||||
|
mock_retriever = Mock(spec=Retriever)
|
||||||
|
mock_retriever.query_relevant_documents.return_value = []
|
||||||
|
|
||||||
|
resources = [Resource(uri="test://uri", title="Test")]
|
||||||
|
tool = RetrieverTool(retriever=mock_retriever, resources=resources)
|
||||||
|
|
||||||
|
result = tool._run("test keywords")
|
||||||
|
|
||||||
|
assert result == "No results found from the local knowledge base."
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_retriever_tool_arun():
|
||||||
|
mock_retriever = Mock(spec=Retriever)
|
||||||
|
chunk = Chunk(content="async content", similarity=0.8)
|
||||||
|
doc = Document(id="doc2", chunks=[chunk])
|
||||||
|
mock_retriever.query_relevant_documents.return_value = [doc]
|
||||||
|
|
||||||
|
resources = [Resource(uri="test://uri", title="Test")]
|
||||||
|
tool = RetrieverTool(retriever=mock_retriever, resources=resources)
|
||||||
|
|
||||||
|
mock_run_manager = Mock(spec=AsyncCallbackManagerForToolRun)
|
||||||
|
mock_sync_manager = Mock(spec=CallbackManagerForToolRun)
|
||||||
|
mock_run_manager.get_sync.return_value = mock_sync_manager
|
||||||
|
|
||||||
|
result = await tool._arun("async keywords", mock_run_manager)
|
||||||
|
|
||||||
|
mock_run_manager.get_sync.assert_called_once()
|
||||||
|
assert isinstance(result, list)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0] == doc.to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
@patch("src.tools.retriever.build_retriever")
|
||||||
|
def test_get_retriever_tool_success(mock_build_retriever):
|
||||||
|
mock_retriever = Mock(spec=Retriever)
|
||||||
|
mock_build_retriever.return_value = mock_retriever
|
||||||
|
|
||||||
|
resources = [Resource(uri="test://uri", title="Test")]
|
||||||
|
tool = get_retriever_tool(resources)
|
||||||
|
|
||||||
|
assert isinstance(tool, RetrieverTool)
|
||||||
|
assert tool.retriever == mock_retriever
|
||||||
|
assert tool.resources == resources
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_retriever_tool_empty_resources():
|
||||||
|
result = get_retriever_tool([])
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
@patch("src.tools.retriever.build_retriever")
|
||||||
|
def test_get_retriever_tool_no_retriever(mock_build_retriever):
|
||||||
|
mock_build_retriever.return_value = None
|
||||||
|
|
||||||
|
resources = [Resource(uri="test://uri", title="Test")]
|
||||||
|
result = get_retriever_tool(resources)
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_retriever_tool_run_with_callback_manager():
|
||||||
|
mock_retriever = Mock(spec=Retriever)
|
||||||
|
mock_retriever.query_relevant_documents.return_value = []
|
||||||
|
|
||||||
|
resources = [Resource(uri="test://uri", title="Test")]
|
||||||
|
tool = RetrieverTool(retriever=mock_retriever, resources=resources)
|
||||||
|
|
||||||
|
mock_callback_manager = Mock(spec=CallbackManagerForToolRun)
|
||||||
|
result = tool._run("test keywords", mock_callback_manager)
|
||||||
|
|
||||||
|
assert result == "No results found from the local knowledge base."
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import json
|
||||||
|
from src.utils.json_utils import repair_json_output
|
||||||
|
|
||||||
|
|
||||||
|
class TestRepairJsonOutput:
|
||||||
|
|
||||||
|
def test_valid_json_object(self):
|
||||||
|
"""Test with valid JSON object"""
|
||||||
|
content = '{"key": "value", "number": 123}'
|
||||||
|
result = repair_json_output(content)
|
||||||
|
expected = json.dumps({"key": "value", "number": 123}, ensure_ascii=False)
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
def test_valid_json_array(self):
|
||||||
|
"""Test with valid JSON array"""
|
||||||
|
content = '[1, 2, 3, "test"]'
|
||||||
|
result = repair_json_output(content)
|
||||||
|
expected = json.dumps([1, 2, 3, "test"], ensure_ascii=False)
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
def test_json_with_code_block_json(self):
|
||||||
|
"""Test JSON wrapped in ```json code block"""
|
||||||
|
content = '```json\n{"key": "value"}\n```'
|
||||||
|
result = repair_json_output(content)
|
||||||
|
expected = json.dumps({"key": "value"}, ensure_ascii=False)
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
def test_json_with_code_block_ts(self):
|
||||||
|
"""Test JSON wrapped in ```ts code block"""
|
||||||
|
content = '```ts\n{"key": "value"}\n```'
|
||||||
|
result = repair_json_output(content)
|
||||||
|
expected = json.dumps({"key": "value"}, ensure_ascii=False)
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
def test_malformed_json_repair(self):
|
||||||
|
"""Test with malformed JSON that can be repaired"""
|
||||||
|
content = '{"key": "value", "incomplete":'
|
||||||
|
result = repair_json_output(content)
|
||||||
|
# Should return repaired JSON
|
||||||
|
assert result.startswith('{"key": "value"')
|
||||||
|
|
||||||
|
def test_non_json_content(self):
|
||||||
|
"""Test with non-JSON content"""
|
||||||
|
content = "This is just plain text"
|
||||||
|
result = repair_json_output(content)
|
||||||
|
assert result == content
|
||||||
|
|
||||||
|
def test_empty_string(self):
|
||||||
|
"""Test with empty string"""
|
||||||
|
content = ""
|
||||||
|
result = repair_json_output(content)
|
||||||
|
assert result == ""
|
||||||
|
|
||||||
|
def test_whitespace_only(self):
|
||||||
|
"""Test with whitespace only"""
|
||||||
|
content = " \n\t "
|
||||||
|
result = repair_json_output(content)
|
||||||
|
assert result == ""
|
||||||
|
|
||||||
|
def test_json_with_unicode(self):
|
||||||
|
"""Test JSON with unicode characters"""
|
||||||
|
content = '{"name": "测试", "emoji": "🎯"}'
|
||||||
|
result = repair_json_output(content)
|
||||||
|
expected = json.dumps({"name": "测试", "emoji": "🎯"}, ensure_ascii=False)
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
def test_json_code_block_without_closing(self):
|
||||||
|
"""Test JSON code block without closing```"""
|
||||||
|
content = '```json\n{"key": "value"}'
|
||||||
|
result = repair_json_output(content)
|
||||||
|
expected = json.dumps({"key": "value"}, ensure_ascii=False)
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
def test_json_repair_broken_json(self):
|
||||||
|
"""Test exception handling when JSON repair fails"""
|
||||||
|
content = '{"this": "is", "completely": broken and unparseable'
|
||||||
|
expect = '{"this": "is", "completely": "broken and unparseable"}'
|
||||||
|
result = repair_json_output(content)
|
||||||
|
assert result == expect
|
||||||
|
|
||||||
|
def test_nested_json_object(self):
|
||||||
|
"""Test with nested JSON object"""
|
||||||
|
content = '{"outer": {"inner": {"deep": "value"}}}'
|
||||||
|
result = repair_json_output(content)
|
||||||
|
expected = json.dumps(
|
||||||
|
{"outer": {"inner": {"deep": "value"}}}, ensure_ascii=False
|
||||||
|
)
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
def test_json_array_with_objects(self):
|
||||||
|
"""Test JSON array containing objects"""
|
||||||
|
content = '[{"id": 1, "name": "test1"}, {"id": 2, "name": "test2"}]'
|
||||||
|
result = repair_json_output(content)
|
||||||
|
expected = json.dumps(
|
||||||
|
[{"id": 1, "name": "test1"}, {"id": 2, "name": "test2"}], ensure_ascii=False
|
||||||
|
)
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
def test_content_with_json_in_middle(self):
|
||||||
|
"""Test content that contains ```json in the middle"""
|
||||||
|
content = 'Some text before ```json {"key": "value"} and after'
|
||||||
|
result = repair_json_output(content)
|
||||||
|
# Should attempt to process as JSON since it contains ```json
|
||||||
|
assert isinstance(result, str)
|
||||||
|
assert result == '{"key": "value"}'
|
||||||
@@ -365,6 +365,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" },
|
{ url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "decorator"
|
||||||
|
version = "5.2.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deer-flow"
|
name = "deer-flow"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -393,6 +402,7 @@ dependencies = [
|
|||||||
{ name = "socksio" },
|
{ name = "socksio" },
|
||||||
{ name = "sse-starlette" },
|
{ name = "sse-starlette" },
|
||||||
{ name = "uvicorn" },
|
{ name = "uvicorn" },
|
||||||
|
{ name = "volcengine" },
|
||||||
{ name = "yfinance" },
|
{ name = "yfinance" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -400,9 +410,11 @@ dependencies = [
|
|||||||
dev = [
|
dev = [
|
||||||
{ name = "black" },
|
{ name = "black" },
|
||||||
{ name = "langgraph-cli", extra = ["inmem"] },
|
{ name = "langgraph-cli", extra = ["inmem"] },
|
||||||
|
{ name = "ruff" },
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-asyncio" },
|
||||||
{ name = "pytest-cov" },
|
{ name = "pytest-cov" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -429,12 +441,15 @@ requires-dist = [
|
|||||||
{ name = "numpy", specifier = ">=2.2.3" },
|
{ name = "numpy", specifier = ">=2.2.3" },
|
||||||
{ name = "pandas", specifier = ">=2.2.3" },
|
{ name = "pandas", specifier = ">=2.2.3" },
|
||||||
{ name = "pytest", marker = "extra == 'test'", specifier = ">=7.4.0" },
|
{ name = "pytest", marker = "extra == 'test'", specifier = ">=7.4.0" },
|
||||||
|
{ name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=1.0.0" },
|
||||||
{ name = "pytest-cov", marker = "extra == 'test'", specifier = ">=4.1.0" },
|
{ name = "pytest-cov", marker = "extra == 'test'", specifier = ">=4.1.0" },
|
||||||
{ name = "python-dotenv", specifier = ">=1.0.1" },
|
{ name = "python-dotenv", specifier = ">=1.0.1" },
|
||||||
{ name = "readabilipy", specifier = ">=0.3.0" },
|
{ name = "readabilipy", specifier = ">=0.3.0" },
|
||||||
|
{ name = "ruff", marker = "extra == 'dev'" },
|
||||||
{ name = "socksio", specifier = ">=1.0.0" },
|
{ name = "socksio", specifier = ">=1.0.0" },
|
||||||
{ name = "sse-starlette", specifier = ">=1.6.5" },
|
{ name = "sse-starlette", specifier = ">=1.6.5" },
|
||||||
{ name = "uvicorn", specifier = ">=0.27.1" },
|
{ name = "uvicorn", specifier = ">=0.27.1" },
|
||||||
|
{ name = "volcengine", specifier = ">=1.0.191" },
|
||||||
{ name = "yfinance", specifier = ">=0.2.54" },
|
{ name = "yfinance", specifier = ">=0.2.54" },
|
||||||
]
|
]
|
||||||
provides-extras = ["dev", "test"]
|
provides-extras = ["dev", "test"]
|
||||||
@@ -562,6 +577,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/56/53/eb690efa8513166adef3e0669afd31e95ffde69fb3c52ec2ac7223ed6018/fsspec-2025.3.0-py3-none-any.whl", hash = "sha256:efb87af3efa9103f94ca91a7f8cb7a4df91af9f74fc106c9c7ea0efd7277c1b3", size = 193615, upload-time = "2025-03-07T21:47:54.809Z" },
|
{ url = "https://files.pythonhosted.org/packages/56/53/eb690efa8513166adef3e0669afd31e95ffde69fb3c52ec2ac7223ed6018/fsspec-2025.3.0-py3-none-any.whl", hash = "sha256:efb87af3efa9103f94ca91a7f8cb7a4df91af9f74fc106c9c7ea0efd7277c1b3", size = 193615, upload-time = "2025-03-07T21:47:54.809Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "google"
|
||||||
|
version = "3.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "beautifulsoup4" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/89/97/b49c69893cddea912c7a660a4b6102c6b02cd268f8c7162dd70b7c16f753/google-3.0.0.tar.gz", hash = "sha256:143530122ee5130509ad5e989f0512f7cb218b2d4eddbafbad40fd10e8d8ccbe", size = 44978, upload-time = "2020-07-11T14:50:45.678Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/35/17c9141c4ae21e9a29a43acdfd848e3e468a810517f862cad07977bf8fe9/google-3.0.0-py2.py3-none-any.whl", hash = "sha256:889cf695f84e4ae2c55fbc0cfdaf4c1e729417fa52ab1db0485202ba173e4935", size = 45258, upload-time = "2020-07-11T14:49:58.287Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "greenlet"
|
name = "greenlet"
|
||||||
version = "3.1.1"
|
version = "3.1.1"
|
||||||
@@ -1593,6 +1620,29 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b5/35/6c4c6fc8774a9e3629cd750dc24a7a4fb090a25ccd5c3246d127b70f9e22/propcache-0.3.0-py3-none-any.whl", hash = "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043", size = 12101, upload-time = "2025-02-20T19:03:27.202Z" },
|
{ url = "https://files.pythonhosted.org/packages/b5/35/6c4c6fc8774a9e3629cd750dc24a7a4fb090a25ccd5c3246d127b70f9e22/propcache-0.3.0-py3-none-any.whl", hash = "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043", size = 12101, upload-time = "2025-02-20T19:03:27.202Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protobuf"
|
||||||
|
version = "6.31.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797, upload-time = "2025-05-28T19:25:54.947Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603, upload-time = "2025-05-28T19:25:41.198Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283, upload-time = "2025-05-28T19:25:44.275Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604, upload-time = "2025-05-28T19:25:45.702Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115, upload-time = "2025-05-28T19:25:47.128Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070, upload-time = "2025-05-28T19:25:50.036Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "py"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796, upload-time = "2021-11-04T17:17:01.377Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708, upload-time = "2021-11-04T17:17:00.152Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "2.22"
|
version = "2.22"
|
||||||
@@ -1602,6 +1652,12 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
|
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycryptodome"
|
||||||
|
version = "3.9.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c4/3a/5bca2cb1648b171afd6b7d29a11c6bca8b305bb75b7e2d78a0f5c61ff95e/pycryptodome-3.9.9.tar.gz", hash = "sha256:910e202a557e1131b1c1b3f17a63914d57aac55cf9fb9b51644962841c3995c4", size = 15488528, upload-time = "2020-11-03T13:15:26.723Z" }
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.10.6"
|
version = "2.10.6"
|
||||||
@@ -1692,6 +1748,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
|
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-asyncio"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960, upload-time = "2025-05-26T04:54:40.484Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976, upload-time = "2025-05-26T04:54:39.035Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest-cov"
|
name = "pytest-cov"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
@@ -1855,6 +1923,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
|
{ url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "retry"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "decorator" },
|
||||||
|
{ name = "py" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9d/72/75d0b85443fbc8d9f38d08d2b1b67cc184ce35280e4a3813cda2f445f3a4/retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4", size = 6448, upload-time = "2016-05-11T13:58:51.541Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/0d/53aea75710af4528a25ed6837d71d117602b01946b307a3912cb3cfcbcba/retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606", size = 7986, upload-time = "2016-05-11T13:58:39.925Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rpds-py"
|
name = "rpds-py"
|
||||||
version = "0.23.1"
|
version = "0.23.1"
|
||||||
@@ -1902,6 +1983,31 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/68/15/6d22d07e063ce5e9bfbd96db9ec2fbb4693591b4503e3a76996639474d02/rpds_py-0.23.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d6f6512a90bd5cd9030a6237f5346f046c6f0e40af98657568fa45695d4de59d", size = 235415, upload-time = "2025-02-21T15:03:12.664Z" },
|
{ url = "https://files.pythonhosted.org/packages/68/15/6d22d07e063ce5e9bfbd96db9ec2fbb4693591b4503e3a76996639474d02/rpds_py-0.23.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d6f6512a90bd5cd9030a6237f5346f046c6f0e40af98657568fa45695d4de59d", size = 235415, upload-time = "2025-02-21T15:03:12.664Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.11.10"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e8/4c/4a3c5a97faaae6b428b336dcca81d03ad04779f8072c267ad2bd860126bf/ruff-0.11.10.tar.gz", hash = "sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6", size = 4165632 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/9f/596c628f8824a2ce4cd12b0f0b4c0629a62dfffc5d0f742c19a1d71be108/ruff-0.11.10-py3-none-linux_armv6l.whl", hash = "sha256:859a7bfa7bc8888abbea31ef8a2b411714e6a80f0d173c2a82f9041ed6b50f58", size = 10316243 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/38/c1e0b77ab58b426f8c332c1d1d3432d9fc9a9ea622806e208220cb133c9e/ruff-0.11.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:968220a57e09ea5e4fd48ed1c646419961a0570727c7e069842edd018ee8afed", size = 11083636 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/41/b75e15961d6047d7fe1b13886e56e8413be8467a4e1be0a07f3b303cd65a/ruff-0.11.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1067245bad978e7aa7b22f67113ecc6eb241dca0d9b696144256c3a879663bca", size = 10441624 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/2c/e396b6703f131406db1811ea3d746f29d91b41bbd43ad572fea30da1435d/ruff-0.11.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4854fd09c7aed5b1590e996a81aeff0c9ff51378b084eb5a0b9cd9518e6cff2", size = 10624358 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/8c/ee6cca8bdaf0f9a3704796022851a33cd37d1340bceaf4f6e991eb164e2e/ruff-0.11.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b4564e9f99168c0f9195a0fd5fa5928004b33b377137f978055e40008a082c5", size = 10176850 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/ce/4e27e131a434321b3b7c66512c3ee7505b446eb1c8a80777c023f7e876e6/ruff-0.11.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b6a9cc5b62c03cc1fea0044ed8576379dbaf751d5503d718c973d5418483641", size = 11759787 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/de/1e2e77fc72adc7cf5b5123fd04a59ed329651d3eab9825674a9e640b100b/ruff-0.11.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:607ecbb6f03e44c9e0a93aedacb17b4eb4f3563d00e8b474298a201622677947", size = 12430479 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/ed/af0f2340f33b70d50121628ef175523cc4c37619e98d98748c85764c8d88/ruff-0.11.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b3a522fa389402cd2137df9ddefe848f727250535c70dafa840badffb56b7a4", size = 11919760 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/09/d7b3d3226d535cb89234390f418d10e00a157b6c4a06dfbe723e9322cb7d/ruff-0.11.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f071b0deed7e9245d5820dac235cbdd4ef99d7b12ff04c330a241ad3534319f", size = 14041747 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/b3/a63b4e91850e3f47f78795e6630ee9266cb6963de8f0191600289c2bb8f4/ruff-0.11.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a60e3a0a617eafba1f2e4186d827759d65348fa53708ca547e384db28406a0b", size = 11550657 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/63/a4f95c241d79402ccdbdb1d823d156c89fbb36ebfc4289dce092e6c0aa8f/ruff-0.11.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:da8ec977eaa4b7bf75470fb575bea2cb41a0e07c7ea9d5a0a97d13dbca697bf2", size = 10489671 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/9b/c2238bfebf1e473495659c523d50b1685258b6345d5ab0b418ca3f010cd7/ruff-0.11.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ddf8967e08227d1bd95cc0851ef80d2ad9c7c0c5aab1eba31db49cf0a7b99523", size = 10160135 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/ef/ba7251dd15206688dbfba7d413c0312e94df3b31b08f5d695580b755a899/ruff-0.11.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5a94acf798a82db188f6f36575d80609072b032105d114b0f98661e1679c9125", size = 11170179 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/9f/5c336717293203ba275dbfa2ea16e49b29a9fd9a0ea8b6febfc17e133577/ruff-0.11.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3afead355f1d16d95630df28d4ba17fb2cb9c8dfac8d21ced14984121f639bad", size = 11626021 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/2b/162fa86d2639076667c9aa59196c020dc6d7023ac8f342416c2f5ec4bda0/ruff-0.11.10-py3-none-win32.whl", hash = "sha256:dc061a98d32a97211af7e7f3fa1d4ca2fcf919fb96c28f39551f35fc55bdbc19", size = 10494958 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/f3/66643d8f32f50a4b0d09a4832b7d919145ee2b944d43e604fbd7c144d175/ruff-0.11.10-py3-none-win_amd64.whl", hash = "sha256:5cc725fbb4d25b0f185cb42df07ab6b76c4489b4bfb740a175f3a59c70e8a224", size = 11650285 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/3a/2e8704d19f376c799748ff9cb041225c1d59f3e7711bc5596c8cfdc24925/ruff-0.11.10-py3-none-win_arm64.whl", hash = "sha256:ef69637b35fb8b210743926778d0e45e1bffa850a7c61e428c6b971549b5f5d1", size = 10765278 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sgmllib3k"
|
name = "sgmllib3k"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -2140,6 +2246,21 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315, upload-time = "2024-12-15T13:33:27.467Z" },
|
{ url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315, upload-time = "2024-12-15T13:33:27.467Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "volcengine"
|
||||||
|
version = "1.0.191"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "google" },
|
||||||
|
{ name = "protobuf" },
|
||||||
|
{ name = "pycryptodome" },
|
||||||
|
{ name = "pytz" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "retry" },
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f4/d8/0ea9b18f216808af709306084d10369f712b98cb5381381d44115dfa6536/volcengine-1.0.191.tar.gz", hash = "sha256:cf3c2dc118c92a7a47f1ab8a48f4789d47e84d17778a1717e1afe9cbce90c986", size = 356076, upload-time = "2025-06-26T12:25:16.353Z" }
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "watchfiles"
|
name = "watchfiles"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"save": "Save",
|
||||||
|
"settings": "Settings",
|
||||||
|
"getStarted": "Get Started",
|
||||||
|
"learnMore": "Learn More",
|
||||||
|
"starOnGitHub": "Star on GitHub",
|
||||||
|
"send": "Send",
|
||||||
|
"stop": "Stop",
|
||||||
|
"linkNotReliable": "This link might be a hallucination from AI model and may not be reliable.",
|
||||||
|
"noResult": "No result"
|
||||||
|
},
|
||||||
|
"messageInput": {
|
||||||
|
"placeholder": "What can I do for you?",
|
||||||
|
"placeholderWithRag": "What can I do for you? \nYou may refer to RAG resources by using @."
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"title": "DeerFlow"
|
||||||
|
},
|
||||||
|
"hero": {
|
||||||
|
"title": "Deep Research",
|
||||||
|
"subtitle": "at Your Fingertips",
|
||||||
|
"description": "Meet DeerFlow, your personal Deep Research assistant. With powerful tools like search engines, web crawlers, Python and MCP services, it delivers instant insights, comprehensive reports, and even captivating podcasts.",
|
||||||
|
"footnote": "* DEER stands for Deep Exploration and Efficient Research."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "DeerFlow Settings",
|
||||||
|
"description": "Manage your DeerFlow settings here.",
|
||||||
|
"addServers": "Add Servers",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"addNewMCPServers": "Add New MCP Servers",
|
||||||
|
"mcpConfigDescription": "DeerFlow uses the standard JSON MCP config to create a new server.",
|
||||||
|
"pasteConfigBelow": "Paste your config below and click \"Add\" to add new servers.",
|
||||||
|
"add": "Add",
|
||||||
|
"general": {
|
||||||
|
"title": "General",
|
||||||
|
"autoAcceptPlan": "Allow automatic acceptance of plans",
|
||||||
|
"maxPlanIterations": "Max plan iterations",
|
||||||
|
"maxPlanIterationsDescription": "Set to 1 for single-step planning. Set to 2 or more to enable re-planning.",
|
||||||
|
"maxStepsOfPlan": "Max steps of a research plan",
|
||||||
|
"maxStepsDescription": "By default, each research plan has 3 steps.",
|
||||||
|
"maxSearchResults": "Max search results",
|
||||||
|
"maxSearchResultsDescription": "By default, each search step has 3 results."
|
||||||
|
},
|
||||||
|
"mcp": {
|
||||||
|
"title": "MCP Servers",
|
||||||
|
"description": "The Model Context Protocol boosts DeerFlow by integrating external tools for tasks like private domain searches, web browsing, food ordering, and more. Click here to",
|
||||||
|
"learnMore": "learn more about MCP.",
|
||||||
|
"enableDisable": "Enable/disable server",
|
||||||
|
"deleteServer": "Delete server",
|
||||||
|
"disabled": "Disabled",
|
||||||
|
"new": "New"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"title": "About"
|
||||||
|
},
|
||||||
|
"reportStyle": {
|
||||||
|
"writingStyle": "Writing Style",
|
||||||
|
"chooseTitle": "Choose Writing Style",
|
||||||
|
"chooseDesc": "Select the writing style for your research reports. Different styles are optimized for different audiences and purposes.",
|
||||||
|
"academic": "Academic",
|
||||||
|
"academicDesc": "Formal, objective, and analytical with precise terminology",
|
||||||
|
"popularScience": "Popular Science",
|
||||||
|
"popularScienceDesc": "Engaging and accessible for general audience",
|
||||||
|
"news": "News",
|
||||||
|
"newsDesc": "Factual, concise, and impartial journalistic style",
|
||||||
|
"socialMedia": "Social Media",
|
||||||
|
"socialMediaDesc": "Concise, attention-grabbing, and shareable"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"quote": "Originated from Open Source, give back to Open Source.",
|
||||||
|
"license": "Licensed under MIT License",
|
||||||
|
"copyright": "DeerFlow"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"page": {
|
||||||
|
"loading": "Loading DeerFlow...",
|
||||||
|
"welcomeUser": "Welcome, {username}",
|
||||||
|
"starOnGitHub": "Star DeerFlow on GitHub"
|
||||||
|
},
|
||||||
|
"welcome": {
|
||||||
|
"greeting": "👋 Hello, there!",
|
||||||
|
"description": "Welcome to 🦌 DeerFlow, a deep research assistant built on cutting-edge language models, helps you search on web, browse information, and handle complex tasks."
|
||||||
|
},
|
||||||
|
"conversationStarters": [
|
||||||
|
"How many times taller is the Eiffel Tower than the tallest building in the world?",
|
||||||
|
"How many years does an average Tesla battery last compared to a gasoline engine?",
|
||||||
|
"How many liters of water are required to produce 1 kg of beef?",
|
||||||
|
"How many times faster is the speed of light compared to the speed of sound?"
|
||||||
|
],
|
||||||
|
"inputBox": {
|
||||||
|
"deepThinking": "Deep Thinking",
|
||||||
|
"deepThinkingTooltip": {
|
||||||
|
"title": "Deep Thinking Mode: {status}",
|
||||||
|
"description": "When enabled, DeerFlow will use reasoning model ({model}) to generate more thoughtful plans."
|
||||||
|
},
|
||||||
|
"investigation": "Investigation",
|
||||||
|
"investigationTooltip": {
|
||||||
|
"title": "Investigation Mode: {status}",
|
||||||
|
"description": "When enabled, DeerFlow will perform a quick search before planning. This is useful for researches related to ongoing events and news."
|
||||||
|
},
|
||||||
|
"enhancePrompt": "Enhance prompt with AI",
|
||||||
|
"on": "On",
|
||||||
|
"off": "Off"
|
||||||
|
},
|
||||||
|
"research": {
|
||||||
|
"deepResearch": "Deep Research",
|
||||||
|
"researching": "Researching...",
|
||||||
|
"generatingReport": "Generating report...",
|
||||||
|
"reportGenerated": "Report generated",
|
||||||
|
"open": "Open",
|
||||||
|
"close": "Close",
|
||||||
|
"deepThinking": "Deep Thinking",
|
||||||
|
"report": "Report",
|
||||||
|
"activities": "Activities",
|
||||||
|
"generatePodcast": "Generate podcast",
|
||||||
|
"edit": "Edit",
|
||||||
|
"copy": "Copy",
|
||||||
|
"downloadReport": "Download report as markdown",
|
||||||
|
"searchingFor": "Searching for",
|
||||||
|
"reading": "Reading",
|
||||||
|
"runningPythonCode": "Running Python code",
|
||||||
|
"errorExecutingCode": "Error when executing the above code",
|
||||||
|
"executionOutput": "Execution output",
|
||||||
|
"retrievingDocuments": "Retrieving documents from RAG",
|
||||||
|
"running": "Running",
|
||||||
|
"generatingPodcast": "Generating podcast...",
|
||||||
|
"nowPlayingPodcast": "Now playing podcast...",
|
||||||
|
"podcast": "Podcast",
|
||||||
|
"errorGeneratingPodcast": "Error when generating podcast. Please try again.",
|
||||||
|
"downloadPodcast": "Download podcast"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"replaying": "Replaying",
|
||||||
|
"replayDescription": "DeerFlow is now replaying the conversation...",
|
||||||
|
"replayHasStopped": "The replay has been stopped.",
|
||||||
|
"replayModeDescription": "You're now in DeerFlow's replay mode. Click the \"Play\" button on the right to start.",
|
||||||
|
"play": "Play",
|
||||||
|
"fastForward": "Fast Forward",
|
||||||
|
"demoNotice": "* This site is for demo purposes only. If you want to try your own question, please",
|
||||||
|
"clickHere": "click here",
|
||||||
|
"cloneLocally": "to clone it locally and run it."
|
||||||
|
},
|
||||||
|
"multiAgent": {
|
||||||
|
"moveToPrevious": "Move to the previous step",
|
||||||
|
"playPause": "Play / Pause",
|
||||||
|
"moveToNext": "Move to the next step",
|
||||||
|
"toggleFullscreen": "Toggle fullscreen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"landing": {
|
||||||
|
"caseStudies": {
|
||||||
|
"title": "Case Studies",
|
||||||
|
"description": "See DeerFlow in action through replays.",
|
||||||
|
"clickToWatch": "Click to watch replay",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"title": "How tall is Eiffel Tower compared to tallest building?",
|
||||||
|
"description": "The research compares the heights and global significance of the Eiffel Tower and Burj Khalifa, and uses Python code to calculate the multiples."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "What are the top trending repositories on GitHub?",
|
||||||
|
"description": "The research utilized MCP services to identify the most popular GitHub repositories and documented them in detail using search engines."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Write an article about Nanjing's traditional dishes",
|
||||||
|
"description": "The study vividly showcases Nanjing's famous dishes through rich content and imagery, uncovering their hidden histories and cultural significance."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "How to decorate a small rental apartment?",
|
||||||
|
"description": "The study provides readers with practical and straightforward methods for decorating apartments, accompanied by inspiring images."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Introduce the movie 'Léon: The Professional'",
|
||||||
|
"description": "The research provides a comprehensive introduction to the movie 'Léon: The Professional', including its plot, characters, and themes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "How do you view the takeaway war in China? (in Chinese)",
|
||||||
|
"description": "The research analyzes the intensifying competition between JD and Meituan, highlighting their strategies, technological innovations, and challenges."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Are ultra-processed foods linked to health?",
|
||||||
|
"description": "The research examines the health risks of rising ultra-processed food consumption, urging more research on long-term effects and individual differences."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Write an article on \"Would you insure your AI twin?\"",
|
||||||
|
"description": "The research explores the concept of insuring AI twins, highlighting their benefits, risks, ethical considerations, and the evolving regulatory."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"coreFeatures": {
|
||||||
|
"title": "Core Features",
|
||||||
|
"description": "Find out what makes DeerFlow effective.",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"name": "Dive Deeper and Reach Wider",
|
||||||
|
"description": "Unlock deeper insights with advanced tools. Our powerful search + crawling and Python tools gathers comprehensive data, delivering in-depth reports to enhance your study."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Human-in-the-loop",
|
||||||
|
"description": "Refine your research plan, or adjust focus areas all through simple natural language."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lang Stack",
|
||||||
|
"description": "Build with confidence using the LangChain and LangGraph frameworks."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MCP Integrations",
|
||||||
|
"description": "Supercharge your research workflow and expand your toolkit with seamless MCP integrations."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Podcast Generation",
|
||||||
|
"description": "Instantly generate podcasts from reports. Perfect for on-the-go learning or sharing findings effortlessly."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"multiAgent": {
|
||||||
|
"title": "Multi-Agent Architecture",
|
||||||
|
"description": "Experience the agent teamwork with our Supervisor + Handoffs design pattern."
|
||||||
|
},
|
||||||
|
"joinCommunity": {
|
||||||
|
"title": "Join the DeerFlow Community",
|
||||||
|
"description": "Contribute brilliant ideas to shape the future of DeerFlow. Collaborate, innovate, and make impacts.",
|
||||||
|
"contributeNow": "Contribute Now"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"cancel": "取消",
|
||||||
|
"save": "保存",
|
||||||
|
"settings": "设置",
|
||||||
|
"getStarted": "开始使用",
|
||||||
|
"learnMore": "了解更多",
|
||||||
|
"starOnGitHub": "在 GitHub 上点赞",
|
||||||
|
"send": "发送",
|
||||||
|
"stop": "停止",
|
||||||
|
"linkNotReliable": "此链接可能是 AI 生成的幻觉,可能并不可靠。",
|
||||||
|
"noResult": "无结果"
|
||||||
|
},
|
||||||
|
"messageInput": {
|
||||||
|
"placeholder": "我能帮你做什么?",
|
||||||
|
"placeholderWithRag": "我能帮你做什么?\n你可以通过 @ 引用 RAG 资源。"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"title": "DeerFlow"
|
||||||
|
},
|
||||||
|
"hero": {
|
||||||
|
"title": "深度研究",
|
||||||
|
"subtitle": "触手可及",
|
||||||
|
"description": "认识 DeerFlow,您的个人深度研究助手。凭借搜索引擎、网络爬虫、Python 和 MCP 服务等强大工具,它能提供即时洞察、全面报告,甚至制作引人入胜的播客。",
|
||||||
|
"footnote": "* DEER 代表深度探索和高效研究。"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "DeerFlow 设置",
|
||||||
|
"description": "在这里管理您的 DeerFlow 设置。",
|
||||||
|
"cancel": "取消",
|
||||||
|
"addServers": "添加服务器",
|
||||||
|
"addNewMCPServers": "添加新的 MCP 服务器",
|
||||||
|
"mcpConfigDescription": "DeerFlow 使用标准 JSON MCP 配置来创建新服务器。",
|
||||||
|
"pasteConfigBelow": "将您的配置粘贴到下面,然后点击\"添加\"来添加新服务器。",
|
||||||
|
"add": "添加",
|
||||||
|
"general": {
|
||||||
|
"title": "通用",
|
||||||
|
"autoAcceptPlan": "允许自动接受计划",
|
||||||
|
"maxPlanIterations": "最大计划迭代次数",
|
||||||
|
"maxPlanIterationsDescription": "设置为 1 进行单步规划。设置为 2 或更多以启用重新规划。",
|
||||||
|
"maxStepsOfPlan": "研究计划的最大步骤数",
|
||||||
|
"maxStepsDescription": "默认情况下,每个研究计划有 3 个步骤。",
|
||||||
|
"maxSearchResults": "最大搜索结果数",
|
||||||
|
"maxSearchResultsDescription": "默认情况下,每个搜索步骤有 3 个结果。"
|
||||||
|
},
|
||||||
|
"mcp": {
|
||||||
|
"title": "MCP 服务器",
|
||||||
|
"description": "模型上下文协议通过集成外部工具来增强 DeerFlow,用于私域搜索、网页浏览、订餐等任务。点击这里",
|
||||||
|
"learnMore": "了解更多关于 MCP 的信息。",
|
||||||
|
"enableDisable": "启用/禁用服务器",
|
||||||
|
"deleteServer": "删除服务器",
|
||||||
|
"disabled": "已禁用",
|
||||||
|
"new": "新增"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"title": "关于"
|
||||||
|
},
|
||||||
|
"reportStyle": {
|
||||||
|
"writingStyle": "写作风格",
|
||||||
|
"chooseTitle": "选择写作风格",
|
||||||
|
"chooseDesc": "请选择您的研究报告的写作风格。不同风格适用于不同受众和用途。",
|
||||||
|
"academic": "学术",
|
||||||
|
"academicDesc": "正式、客观、分析性强,术语精确",
|
||||||
|
"popularScience": "科普",
|
||||||
|
"popularScienceDesc": "生动有趣,适合大众阅读",
|
||||||
|
"news": "新闻",
|
||||||
|
"newsDesc": "事实、简明、公正的新闻风格",
|
||||||
|
"socialMedia": "社交媒体",
|
||||||
|
"socialMediaDesc": "简洁有趣,易于传播"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"quote": "源于开源,回馈开源。",
|
||||||
|
"license": "基于 MIT 许可证授权",
|
||||||
|
"copyright": "DeerFlow"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"page": {
|
||||||
|
"loading": "正在加载 DeerFlow...",
|
||||||
|
"welcomeUser": "欢迎,{username}",
|
||||||
|
"starOnGitHub": "在 GitHub 上点赞"
|
||||||
|
},
|
||||||
|
"welcome": {
|
||||||
|
"greeting": "👋 你好!",
|
||||||
|
"description": "欢迎来到 🦌 DeerFlow,一个基于前沿语言模型构建的深度研究助手,帮助您搜索网络、浏览信息并处理复杂任务。"
|
||||||
|
},
|
||||||
|
"conversationStarters": [
|
||||||
|
"埃菲尔铁塔比世界最高建筑高多少倍?",
|
||||||
|
"特斯拉电池的平均寿命比汽油发动机长多少年?",
|
||||||
|
"生产1公斤牛肉需要多少升水?",
|
||||||
|
"光速比声速快多少倍?"
|
||||||
|
],
|
||||||
|
"inputBox": {
|
||||||
|
"deepThinking": "深度思考",
|
||||||
|
"deepThinkingTooltip": {
|
||||||
|
"title": "深度思考模式:{status}",
|
||||||
|
"description": "启用后,DeerFlow 将使用推理模型({model})生成更深思熟虑的计划。"
|
||||||
|
},
|
||||||
|
"investigation": "调研",
|
||||||
|
"investigationTooltip": {
|
||||||
|
"title": "调研模式:{status}",
|
||||||
|
"description": "启用后,DeerFlow 将在规划前进行快速搜索。这对于与时事和新闻相关的研究很有用。"
|
||||||
|
},
|
||||||
|
"enhancePrompt": "用 AI 增强提示",
|
||||||
|
"on": "开启",
|
||||||
|
"off": "关闭"
|
||||||
|
},
|
||||||
|
"research": {
|
||||||
|
"deepResearch": "深度研究",
|
||||||
|
"researching": "研究中...",
|
||||||
|
"generatingReport": "生成报告中...",
|
||||||
|
"reportGenerated": "报告已生成",
|
||||||
|
"open": "打开",
|
||||||
|
"close": "关闭",
|
||||||
|
"deepThinking": "深度思考",
|
||||||
|
"report": "报告",
|
||||||
|
"activities": "活动",
|
||||||
|
"generatePodcast": "生成播客",
|
||||||
|
"edit": "编辑",
|
||||||
|
"copy": "复制",
|
||||||
|
"downloadReport": "下载报告为 Markdown",
|
||||||
|
"searchingFor": "搜索",
|
||||||
|
"reading": "阅读中",
|
||||||
|
"runningPythonCode": "运行 Python 代码",
|
||||||
|
"errorExecutingCode": "执行上述代码时出错",
|
||||||
|
"executionOutput": "执行输出",
|
||||||
|
"retrievingDocuments": "从 RAG 检索文档",
|
||||||
|
"running": "运行",
|
||||||
|
"generatingPodcast": "生成播客中...",
|
||||||
|
"nowPlayingPodcast": "正在播放播客...",
|
||||||
|
"podcast": "播客",
|
||||||
|
"errorGeneratingPodcast": "生成播客时出错。请重试。",
|
||||||
|
"downloadPodcast": "下载播客"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"replaying": "回放中",
|
||||||
|
"replayDescription": "DeerFlow 正在回放对话...",
|
||||||
|
"replayHasStopped": "回放已停止。",
|
||||||
|
"replayModeDescription": "您现在处于 DeerFlow 的回放模式。点击右侧的\"播放\"按钮开始。",
|
||||||
|
"play": "播放",
|
||||||
|
"fastForward": "快进",
|
||||||
|
"demoNotice": "* 此网站仅用于演示目的。如果您想尝试自己的问题,请",
|
||||||
|
"clickHere": "点击这里",
|
||||||
|
"cloneLocally": "在本地克隆并运行它。"
|
||||||
|
},
|
||||||
|
"multiAgent": {
|
||||||
|
"moveToPrevious": "移动到上一步",
|
||||||
|
"playPause": "播放 / 暂停",
|
||||||
|
"moveToNext": "移动到下一步",
|
||||||
|
"toggleFullscreen": "切换全屏"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"landing": {
|
||||||
|
"caseStudies": {
|
||||||
|
"title": "案例研究",
|
||||||
|
"description": "通过回放查看 DeerFlow 的实际应用。",
|
||||||
|
"clickToWatch": "点击观看回放",
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"title": "埃菲尔铁塔与最高建筑相比有多高?",
|
||||||
|
"description": "该研究比较了埃菲尔铁塔和哈利法塔的高度和全球意义,并使用 Python 代码计算倍数。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "GitHub 上最热门的仓库有哪些?",
|
||||||
|
"description": "该研究利用 MCP 服务识别最受欢迎的 GitHub 仓库,并使用搜索引擎详细记录它们。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "写一篇关于南京传统菜肴的文章",
|
||||||
|
"description": "该研究通过丰富的内容和图像生动地展示了南京的著名菜肴,揭示了它们隐藏的历史和文化意义。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "如何装饰小型出租公寓?",
|
||||||
|
"description": "该研究为读者提供了实用而直接的公寓装饰方法,并配有鼓舞人心的图像。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "介绍电影《这个杀手不太冷》",
|
||||||
|
"description": "该研究全面介绍了电影《这个杀手不太冷》,包括其情节、角色和主题。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "你如何看待中国的外卖大战?(中文)",
|
||||||
|
"description": "该研究分析了京东和美团之间日益激烈的竞争,突出了它们的策略、技术创新和挑战。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "超加工食品与健康有关吗?",
|
||||||
|
"description": "该研究检查了超加工食品消费增加的健康风险,敦促对长期影响和个体差异进行更多研究。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "写一篇关于\"你会为你的 AI 双胞胎投保吗?\"的文章",
|
||||||
|
"description": "该研究探讨了为 AI 双胞胎投保的概念,突出了它们的好处、风险、伦理考虑和不断发展的监管。"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"coreFeatures": {
|
||||||
|
"title": "核心功能",
|
||||||
|
"description": "了解是什么让 DeerFlow 如此有效。",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"name": "深入挖掘,触及更广",
|
||||||
|
"description": "使用高级工具解锁更深层的洞察。我们强大的搜索+爬取和 Python 工具收集全面的数据,提供深入的报告来增强您的研究。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "人机协作",
|
||||||
|
"description": "通过简单的自然语言完善您的研究计划或调整重点领域。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lang 技术栈",
|
||||||
|
"description": "使用 LangChain 和 LangGraph 框架自信地构建。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MCP 集成",
|
||||||
|
"description": "通过无缝的 MCP 集成增强您的研究工作流程并扩展您的工具包。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "播客生成",
|
||||||
|
"description": "从报告中即时生成播客。非常适合移动学习或轻松分享发现。"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"multiAgent": {
|
||||||
|
"title": "多智能体架构",
|
||||||
|
"description": "通过我们的监督者 + 交接设计模式体验智能体团队合作。"
|
||||||
|
},
|
||||||
|
"joinCommunity": {
|
||||||
|
"title": "加入 DeerFlow 社区",
|
||||||
|
"description": "贡献精彩想法,塑造 DeerFlow 的未来。协作、创新并产生影响。",
|
||||||
|
"contributeNow": "立即贡献"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-1
@@ -6,6 +6,9 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import "./src/env.js";
|
import "./src/env.js";
|
||||||
|
import createNextIntlPlugin from 'next-intl/plugin';
|
||||||
|
|
||||||
|
const withNextIntl = createNextIntlPlugin('./src/i18n.ts');
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
|
|
||||||
@@ -39,4 +42,4 @@ const config = {
|
|||||||
output: "standalone",
|
output: "standalone",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default withNextIntl(config);
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
"@tiptap/extension-table-row": "^2.11.7",
|
"@tiptap/extension-table-row": "^2.11.7",
|
||||||
"@tiptap/extension-text": "^2.12.0",
|
"@tiptap/extension-text": "^2.12.0",
|
||||||
"@tiptap/react": "^2.11.7",
|
"@tiptap/react": "^2.11.7",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@xyflow/react": "^12.6.0",
|
"@xyflow/react": "^12.6.0",
|
||||||
"best-effort-json-parser": "^1.1.3",
|
"best-effort-json-parser": "^1.1.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
"hast": "^1.0.0",
|
"hast": "^1.0.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"katex": "^0.16.21",
|
"katex": "^0.16.21",
|
||||||
"lowlight": "^3.3.0",
|
"lowlight": "^3.3.0",
|
||||||
"lru-cache": "^11.1.0",
|
"lru-cache": "^11.1.0",
|
||||||
@@ -62,6 +64,7 @@
|
|||||||
"motion": "^12.7.4",
|
"motion": "^12.7.4",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next": "^15.2.3",
|
"next": "^15.2.3",
|
||||||
|
"next-intl": "^4.3.1",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"novel": "^1.0.2",
|
"novel": "^1.0.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|||||||
Generated
+126
@@ -95,6 +95,9 @@ importers:
|
|||||||
'@tiptap/react':
|
'@tiptap/react':
|
||||||
specifier: ^2.11.7
|
specifier: ^2.11.7
|
||||||
version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
'@types/js-cookie':
|
||||||
|
specifier: ^3.0.6
|
||||||
|
version: 3.0.6
|
||||||
'@xyflow/react':
|
'@xyflow/react':
|
||||||
specifier: ^12.6.0
|
specifier: ^12.6.0
|
||||||
version: 12.6.0(@types/react@19.1.2)(immer@10.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 12.6.0(@types/react@19.1.2)(immer@10.1.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
@@ -122,6 +125,9 @@ importers:
|
|||||||
immer:
|
immer:
|
||||||
specifier: ^10.1.1
|
specifier: ^10.1.1
|
||||||
version: 10.1.1
|
version: 10.1.1
|
||||||
|
js-cookie:
|
||||||
|
specifier: ^3.0.5
|
||||||
|
version: 3.0.5
|
||||||
katex:
|
katex:
|
||||||
specifier: ^0.16.21
|
specifier: ^0.16.21
|
||||||
version: 0.16.21
|
version: 0.16.21
|
||||||
@@ -143,6 +149,9 @@ importers:
|
|||||||
next:
|
next:
|
||||||
specifier: ^15.2.3
|
specifier: ^15.2.3
|
||||||
version: 15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
next-intl:
|
||||||
|
specifier: ^4.3.1
|
||||||
|
version: 4.3.1(next@15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
|
||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.4.6
|
specifier: ^0.4.6
|
||||||
version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
@@ -367,6 +376,24 @@ packages:
|
|||||||
'@floating-ui/utils@0.2.9':
|
'@floating-ui/utils@0.2.9':
|
||||||
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
|
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
|
||||||
|
|
||||||
|
'@formatjs/ecma402-abstract@2.3.4':
|
||||||
|
resolution: {integrity: sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==}
|
||||||
|
|
||||||
|
'@formatjs/fast-memoize@2.2.7':
|
||||||
|
resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==}
|
||||||
|
|
||||||
|
'@formatjs/icu-messageformat-parser@2.11.2':
|
||||||
|
resolution: {integrity: sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==}
|
||||||
|
|
||||||
|
'@formatjs/icu-skeleton-parser@1.8.14':
|
||||||
|
resolution: {integrity: sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==}
|
||||||
|
|
||||||
|
'@formatjs/intl-localematcher@0.5.10':
|
||||||
|
resolution: {integrity: sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==}
|
||||||
|
|
||||||
|
'@formatjs/intl-localematcher@0.6.1':
|
||||||
|
resolution: {integrity: sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==}
|
||||||
|
|
||||||
'@hookform/resolvers@5.0.1':
|
'@hookform/resolvers@5.0.1':
|
||||||
resolution: {integrity: sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==}
|
resolution: {integrity: sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1285,6 +1312,9 @@ packages:
|
|||||||
'@scena/matrix@1.1.1':
|
'@scena/matrix@1.1.1':
|
||||||
resolution: {integrity: sha512-JVKBhN0tm2Srl+Yt+Ywqu0oLgLcdemDQlD1OxmN9jaCTwaFPZ7tY8n6dhVgMEaR9qcR7r+kAlMXnSfNyYdE+Vg==}
|
resolution: {integrity: sha512-JVKBhN0tm2Srl+Yt+Ywqu0oLgLcdemDQlD1OxmN9jaCTwaFPZ7tY8n6dhVgMEaR9qcR7r+kAlMXnSfNyYdE+Vg==}
|
||||||
|
|
||||||
|
'@schummar/icu-type-parser@1.21.5':
|
||||||
|
resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==}
|
||||||
|
|
||||||
'@standard-schema/utils@0.3.0':
|
'@standard-schema/utils@0.3.0':
|
||||||
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||||
|
|
||||||
@@ -1680,6 +1710,9 @@ packages:
|
|||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||||
|
|
||||||
|
'@types/js-cookie@3.0.6':
|
||||||
|
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
|
||||||
|
|
||||||
'@types/json-schema@7.0.15':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
@@ -2264,6 +2297,9 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
decimal.js@10.5.0:
|
||||||
|
resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==}
|
||||||
|
|
||||||
decode-named-character-reference@1.1.0:
|
decode-named-character-reference@1.1.0:
|
||||||
resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==}
|
resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==}
|
||||||
|
|
||||||
@@ -2752,6 +2788,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
intl-messageformat@10.7.16:
|
||||||
|
resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==}
|
||||||
|
|
||||||
is-alphabetical@1.0.4:
|
is-alphabetical@1.0.4:
|
||||||
resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==}
|
resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==}
|
||||||
|
|
||||||
@@ -2912,6 +2951,10 @@ packages:
|
|||||||
react:
|
react:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
js-cookie@3.0.5:
|
||||||
|
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
@@ -3305,9 +3348,23 @@ packages:
|
|||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
|
negotiator@1.0.0:
|
||||||
|
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
neo-async@2.6.2:
|
neo-async@2.6.2:
|
||||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||||
|
|
||||||
|
next-intl@4.3.1:
|
||||||
|
resolution: {integrity: sha512-FylHpOoQw5MpOyJt4cw8pNEGba7r3jKDSqt112fmBqXVceGR5YncmqpxS5MvSHsWRwbjqpOV8OsZCIY/4f4HWg==}
|
||||||
|
peerDependencies:
|
||||||
|
next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
|
||||||
|
typescript: ^5.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
next-themes@0.4.6:
|
next-themes@0.4.6:
|
||||||
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4128,6 +4185,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '*'
|
react: '*'
|
||||||
|
|
||||||
|
use-intl@4.3.1:
|
||||||
|
resolution: {integrity: sha512-8Xn5RXzeHZhWqqZimi1wi2pKFqm0NxRUOB41k1QdjbPX+ysoeLW3Ey+fi603D/e5EGb0fYw8WzjgtUagJdlIvg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
|
||||||
|
|
||||||
use-sidecar@1.1.3:
|
use-sidecar@1.1.3:
|
||||||
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
|
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -4378,6 +4440,36 @@ snapshots:
|
|||||||
|
|
||||||
'@floating-ui/utils@0.2.9': {}
|
'@floating-ui/utils@0.2.9': {}
|
||||||
|
|
||||||
|
'@formatjs/ecma402-abstract@2.3.4':
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/fast-memoize': 2.2.7
|
||||||
|
'@formatjs/intl-localematcher': 0.6.1
|
||||||
|
decimal.js: 10.5.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@formatjs/fast-memoize@2.2.7':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@formatjs/icu-messageformat-parser@2.11.2':
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/ecma402-abstract': 2.3.4
|
||||||
|
'@formatjs/icu-skeleton-parser': 1.8.14
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@formatjs/icu-skeleton-parser@1.8.14':
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/ecma402-abstract': 2.3.4
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@formatjs/intl-localematcher@0.5.10':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@formatjs/intl-localematcher@0.6.1':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@hookform/resolvers@5.0.1(react-hook-form@7.56.1(react@19.1.0))':
|
'@hookform/resolvers@5.0.1(react-hook-form@7.56.1(react@19.1.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@standard-schema/utils': 0.3.0
|
'@standard-schema/utils': 0.3.0
|
||||||
@@ -5248,6 +5340,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@daybrush/utils': 1.13.0
|
'@daybrush/utils': 1.13.0
|
||||||
|
|
||||||
|
'@schummar/icu-type-parser@1.21.5': {}
|
||||||
|
|
||||||
'@standard-schema/utils@0.3.0': {}
|
'@standard-schema/utils@0.3.0': {}
|
||||||
|
|
||||||
'@swc/counter@0.1.3': {}
|
'@swc/counter@0.1.3': {}
|
||||||
@@ -5633,6 +5727,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
|
|
||||||
|
'@types/js-cookie@3.0.6': {}
|
||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
'@types/json5@0.0.29': {}
|
'@types/json5@0.0.29': {}
|
||||||
@@ -6256,6 +6352,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
decimal.js@10.5.0: {}
|
||||||
|
|
||||||
decode-named-character-reference@1.1.0:
|
decode-named-character-reference@1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
character-entities: 2.0.2
|
character-entities: 2.0.2
|
||||||
@@ -6919,6 +7017,13 @@ snapshots:
|
|||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
side-channel: 1.1.0
|
side-channel: 1.1.0
|
||||||
|
|
||||||
|
intl-messageformat@10.7.16:
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/ecma402-abstract': 2.3.4
|
||||||
|
'@formatjs/fast-memoize': 2.2.7
|
||||||
|
'@formatjs/icu-messageformat-parser': 2.11.2
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
is-alphabetical@1.0.4: {}
|
is-alphabetical@1.0.4: {}
|
||||||
|
|
||||||
is-alphabetical@2.0.1: {}
|
is-alphabetical@2.0.1: {}
|
||||||
@@ -7081,6 +7186,8 @@ snapshots:
|
|||||||
'@types/react': 19.1.2
|
'@types/react': 19.1.2
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
|
|
||||||
|
js-cookie@3.0.5: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
js-yaml@4.1.0:
|
js-yaml@4.1.0:
|
||||||
@@ -7660,8 +7767,20 @@ snapshots:
|
|||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
|
negotiator@1.0.0: {}
|
||||||
|
|
||||||
neo-async@2.6.2: {}
|
neo-async@2.6.2: {}
|
||||||
|
|
||||||
|
next-intl@4.3.1(next@15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3):
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/intl-localematcher': 0.5.10
|
||||||
|
negotiator: 1.0.0
|
||||||
|
next: 15.3.0(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
react: 19.1.0
|
||||||
|
use-intl: 4.3.1(react@19.1.0)
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 5.8.3
|
||||||
|
|
||||||
next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
@@ -8737,6 +8856,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
|
|
||||||
|
use-intl@4.3.1(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
'@formatjs/fast-memoize': 2.2.7
|
||||||
|
'@schummar/icu-type-parser': 1.21.5
|
||||||
|
intl-messageformat: 10.7.16
|
||||||
|
react: 19.1.0
|
||||||
|
|
||||||
use-sidecar@1.1.3(@types/react@19.1.2)(react@19.1.0):
|
use-sidecar@1.1.3(@types/react@19.1.2)(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
detect-node-es: 1.1.0
|
detect-node-es: 1.1.0
|
||||||
|
|||||||
@@ -2,17 +2,12 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
import { Welcome } from "./welcome";
|
import { Welcome } from "./welcome";
|
||||||
|
|
||||||
const questions = [
|
|
||||||
"How many times taller is the Eiffel Tower than the tallest building in the world?",
|
|
||||||
"How many years does an average Tesla battery last compared to a gasoline engine?",
|
|
||||||
"How many liters of water are required to produce 1 kg of beef?",
|
|
||||||
"How many times faster is the speed of light compared to the speed of sound?",
|
|
||||||
];
|
|
||||||
export function ConversationStarter({
|
export function ConversationStarter({
|
||||||
className,
|
className,
|
||||||
onSend,
|
onSend,
|
||||||
@@ -20,6 +15,9 @@ export function ConversationStarter({
|
|||||||
className?: string;
|
className?: string;
|
||||||
onSend?: (message: string) => void;
|
onSend?: (message: string) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("chat");
|
||||||
|
const questions = t.raw("conversationStarters") as string[];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-col items-center", className)}>
|
<div className={cn("flex flex-col items-center", className)}>
|
||||||
<div className="pointer-events-none fixed inset-0 flex items-center justify-center">
|
<div className="pointer-events-none fixed inset-0 flex items-center justify-center">
|
||||||
@@ -41,7 +39,7 @@ export function ConversationStarter({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="bg-card text-muted-foreground cursor-pointer rounded-2xl border px-4 py-4 opacity-75 transition-all duration-300 hover:opacity-100 hover:shadow-md"
|
className="bg-card text-muted-foreground h-full w-full cursor-pointer rounded-2xl border px-4 py-4 opacity-75 transition-all duration-300 hover:opacity-100 hover:shadow-md"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSend?.(question);
|
onSend?.(question);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { MagicWandIcon } from "@radix-ui/react-icons";
|
import { MagicWandIcon } from "@radix-ui/react-icons";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { ArrowUp, Lightbulb, X } from "lucide-react";
|
import { ArrowUp, Lightbulb, X } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { Detective } from "~/components/deer-flow/icons/detective";
|
import { Detective } from "~/components/deer-flow/icons/detective";
|
||||||
@@ -15,7 +16,7 @@ import { Tooltip } from "~/components/deer-flow/tooltip";
|
|||||||
import { BorderBeam } from "~/components/magicui/border-beam";
|
import { BorderBeam } from "~/components/magicui/border-beam";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { enhancePrompt } from "~/core/api";
|
import { enhancePrompt } from "~/core/api";
|
||||||
import { getConfig } from "~/core/api/config";
|
import { useConfig } from "~/core/api/hooks";
|
||||||
import type { Option, Resource } from "~/core/messages";
|
import type { Option, Resource } from "~/core/messages";
|
||||||
import {
|
import {
|
||||||
setEnableDeepThinking,
|
setEnableDeepThinking,
|
||||||
@@ -46,13 +47,15 @@ export function InputBox({
|
|||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onRemoveFeedback?: () => void;
|
onRemoveFeedback?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("chat.inputBox");
|
||||||
|
const tCommon = useTranslations("common");
|
||||||
const enableDeepThinking = useSettingsStore(
|
const enableDeepThinking = useSettingsStore(
|
||||||
(state) => state.general.enableDeepThinking,
|
(state) => state.general.enableDeepThinking,
|
||||||
);
|
);
|
||||||
const backgroundInvestigation = useSettingsStore(
|
const backgroundInvestigation = useSettingsStore(
|
||||||
(state) => state.general.enableBackgroundInvestigation,
|
(state) => state.general.enableBackgroundInvestigation,
|
||||||
);
|
);
|
||||||
const reasoningModel = useMemo(() => getConfig().models.reasoning?.[0], []);
|
const { config, loading } = useConfig();
|
||||||
const reportStyle = useSettingsStore((state) => state.general.reportStyle);
|
const reportStyle = useSettingsStore((state) => state.general.reportStyle);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<MessageInputRef>(null);
|
const inputRef = useRef<MessageInputRef>(null);
|
||||||
@@ -203,23 +206,28 @@ export function InputBox({
|
|||||||
isEnhanceAnimating && "transition-all duration-500",
|
isEnhanceAnimating && "transition-all duration-500",
|
||||||
)}
|
)}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
loading={loading}
|
||||||
|
config={config}
|
||||||
onEnter={handleSendMessage}
|
onEnter={handleSendMessage}
|
||||||
onChange={setCurrentPrompt}
|
onChange={setCurrentPrompt}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center px-4 py-2">
|
<div className="flex items-center px-4 py-2">
|
||||||
<div className="flex grow gap-2">
|
<div className="flex grow gap-2">
|
||||||
{reasoningModel && (
|
{config?.models.reasoning?.[0] && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className="max-w-60"
|
className="max-w-60"
|
||||||
title={
|
title={
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-2 font-bold">
|
<h3 className="mb-2 font-bold">
|
||||||
Deep Thinking Mode: {enableDeepThinking ? "On" : "Off"}
|
{t("deepThinkingTooltip.title", {
|
||||||
|
status: enableDeepThinking ? t("on") : t("off"),
|
||||||
|
})}
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
When enabled, DeerFlow will use reasoning model (
|
{t("deepThinkingTooltip.description", {
|
||||||
{reasoningModel}) to generate more thoughtful plans.
|
model: config.models.reasoning?.[0] ?? "",
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -234,7 +242,7 @@ export function InputBox({
|
|||||||
setEnableDeepThinking(!enableDeepThinking);
|
setEnableDeepThinking(!enableDeepThinking);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Lightbulb /> Deep Thinking
|
<Lightbulb /> {t("deepThinking")}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@@ -244,13 +252,11 @@ export function InputBox({
|
|||||||
title={
|
title={
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-2 font-bold">
|
<h3 className="mb-2 font-bold">
|
||||||
Investigation Mode: {backgroundInvestigation ? "On" : "Off"}
|
{t("investigationTooltip.title", {
|
||||||
|
status: backgroundInvestigation ? t("on") : t("off"),
|
||||||
|
})}
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<p>{t("investigationTooltip.description")}</p>
|
||||||
When enabled, DeerFlow will perform a quick search before
|
|
||||||
planning. This is useful for researches related to ongoing
|
|
||||||
events and news.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -264,13 +270,13 @@ export function InputBox({
|
|||||||
setEnableBackgroundInvestigation(!backgroundInvestigation)
|
setEnableBackgroundInvestigation(!backgroundInvestigation)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Detective /> Investigation
|
<Detective /> {t("investigation")}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ReportStyleDialog />
|
<ReportStyleDialog />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
<div className="flex shrink-0 items-center gap-2">
|
||||||
<Tooltip title="Enhance prompt with AI">
|
<Tooltip title={t("enhancePrompt")}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -290,7 +296,7 @@ export function InputBox({
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={responding ? "Stop" : "Send"}>
|
<Tooltip title={responding ? tCommon("stop") : tCommon("send")}>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
Lightbulb,
|
Lightbulb,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import React, { useCallback, useMemo, useRef, useState } from "react";
|
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { LoadingAnimation } from "~/components/deer-flow/loading-animation";
|
import { LoadingAnimation } from "~/components/deer-flow/loading-animation";
|
||||||
@@ -252,6 +253,7 @@ function ResearchCard({
|
|||||||
researchId: string;
|
researchId: string;
|
||||||
onToggleResearch?: () => void;
|
onToggleResearch?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("chat.research");
|
||||||
const reportId = useStore((state) => state.researchReportIds.get(researchId));
|
const reportId = useStore((state) => state.researchReportIds.get(researchId));
|
||||||
const hasReport = reportId !== undefined;
|
const hasReport = reportId !== undefined;
|
||||||
const reportGenerating = useStore(
|
const reportGenerating = useStore(
|
||||||
@@ -260,10 +262,10 @@ function ResearchCard({
|
|||||||
const openResearchId = useStore((state) => state.openResearchId);
|
const openResearchId = useStore((state) => state.openResearchId);
|
||||||
const state = useMemo(() => {
|
const state = useMemo(() => {
|
||||||
if (hasReport) {
|
if (hasReport) {
|
||||||
return reportGenerating ? "Generating report..." : "Report generated";
|
return reportGenerating ? t("generatingReport") : t("reportGenerated");
|
||||||
}
|
}
|
||||||
return "Researching...";
|
return t("researching");
|
||||||
}, [hasReport, reportGenerating]);
|
}, [hasReport, reportGenerating, t]);
|
||||||
const msg = useResearchMessage(researchId);
|
const msg = useResearchMessage(researchId);
|
||||||
const title = useMemo(() => {
|
const title = useMemo(() => {
|
||||||
if (msg) {
|
if (msg) {
|
||||||
@@ -283,8 +285,8 @@ function ResearchCard({
|
|||||||
<Card className={cn("w-full", className)}>
|
<Card className={cn("w-full", className)}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
<RainbowText animated={state !== "Report generated"}>
|
<RainbowText animated={state !== t("reportGenerated")}>
|
||||||
{title !== undefined && title !== "" ? title : "Deep Research"}
|
{title !== undefined && title !== "" ? title : t("deepResearch")}
|
||||||
</RainbowText>
|
</RainbowText>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -297,7 +299,7 @@ function ResearchCard({
|
|||||||
variant={!openResearchId ? "default" : "outline"}
|
variant={!openResearchId ? "default" : "outline"}
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
>
|
>
|
||||||
{researchId !== openResearchId ? "Open" : "Close"}
|
{researchId !== openResearchId ? t("open") : t("close")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
@@ -316,6 +318,7 @@ function ThoughtBlock({
|
|||||||
isStreaming?: boolean;
|
isStreaming?: boolean;
|
||||||
hasMainContent?: boolean;
|
hasMainContent?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("chat.research");
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
|
||||||
const [hasAutoCollapsed, setHasAutoCollapsed] = useState(false);
|
const [hasAutoCollapsed, setHasAutoCollapsed] = useState(false);
|
||||||
@@ -359,7 +362,7 @@ function ThoughtBlock({
|
|||||||
isStreaming ? "text-primary" : "text-foreground",
|
isStreaming ? "text-primary" : "text-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Deep Thinking
|
{t("deepThinking")}
|
||||||
</span>
|
</span>
|
||||||
{isStreaming && <LoadingAnimation className="ml-2 scale-75" />}
|
{isStreaming && <LoadingAnimation className="ml-2 scale-75" />}
|
||||||
<div className="flex-grow" />
|
<div className="flex-grow" />
|
||||||
@@ -432,6 +435,7 @@ function PlanCard({
|
|||||||
) => void;
|
) => void;
|
||||||
waitForFeedback?: boolean;
|
waitForFeedback?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("chat.research");
|
||||||
const plan = useMemo<{
|
const plan = useMemo<{
|
||||||
title?: string;
|
title?: string;
|
||||||
thought?: string;
|
thought?: string;
|
||||||
@@ -482,7 +486,7 @@ function PlanCard({
|
|||||||
{`### ${
|
{`### ${
|
||||||
plan.title !== undefined && plan.title !== ""
|
plan.title !== undefined && plan.title !== ""
|
||||||
? plan.title
|
? plan.title
|
||||||
: "Deep Research"
|
: t("deepResearch")
|
||||||
}`}
|
}`}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { FastForward, Play } from "lucide-react";
|
import { FastForward, Play } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
|
||||||
import { RainbowText } from "~/components/deer-flow/rainbow-text";
|
import { RainbowText } from "~/components/deer-flow/rainbow-text";
|
||||||
@@ -27,6 +28,7 @@ import { MessageListView } from "./message-list-view";
|
|||||||
import { Welcome } from "./welcome";
|
import { Welcome } from "./welcome";
|
||||||
|
|
||||||
export function MessagesBlock({ className }: { className?: string }) {
|
export function MessagesBlock({ className }: { className?: string }) {
|
||||||
|
const t = useTranslations("chat.messages");
|
||||||
const messageIds = useMessageIds();
|
const messageIds = useMessageIds();
|
||||||
const messageCount = messageIds.length;
|
const messageCount = messageIds.length;
|
||||||
const responding = useStore((state) => state.responding);
|
const responding = useStore((state) => state.responding);
|
||||||
@@ -152,16 +154,16 @@ export function MessagesBlock({ className }: { className?: string }) {
|
|||||||
<CardHeader className={cn("flex-grow", responding && "pl-3")}>
|
<CardHeader className={cn("flex-grow", responding && "pl-3")}>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
<RainbowText animated={responding}>
|
<RainbowText animated={responding}>
|
||||||
{responding ? "Replaying" : `${replayTitle}`}
|
{responding ? t("replaying") : `${replayTitle}`}
|
||||||
</RainbowText>
|
</RainbowText>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
<RainbowText animated={responding}>
|
<RainbowText animated={responding}>
|
||||||
{responding
|
{responding
|
||||||
? "DeerFlow is now replaying the conversation..."
|
? t("replayDescription")
|
||||||
: replayStarted
|
: replayStarted
|
||||||
? "The replay has been stopped."
|
? t("replayHasStopped")
|
||||||
: `You're now in DeerFlow's replay mode. Click the "Play" button on the right to start.`}
|
: t("replayModeDescription")}
|
||||||
</RainbowText>
|
</RainbowText>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -175,13 +177,13 @@ export function MessagesBlock({ className }: { className?: string }) {
|
|||||||
onClick={handleFastForwardReplay}
|
onClick={handleFastForwardReplay}
|
||||||
>
|
>
|
||||||
<FastForward size={16} />
|
<FastForward size={16} />
|
||||||
Fast Forward
|
{t("fastForward")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!replayStarted && (
|
{!replayStarted && (
|
||||||
<Button className="w-24" onClick={handleStartReplay}>
|
<Button className="w-24" onClick={handleStartReplay}>
|
||||||
<Play size={16} />
|
<Play size={16} />
|
||||||
Play
|
{t("play")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -190,17 +192,16 @@ export function MessagesBlock({ className }: { className?: string }) {
|
|||||||
</Card>
|
</Card>
|
||||||
{!replayStarted && env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY && (
|
{!replayStarted && env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY && (
|
||||||
<div className="text-muted-foreground w-full text-center text-xs">
|
<div className="text-muted-foreground w-full text-center text-xs">
|
||||||
* This site is for demo purposes only. If you want to try your
|
{t("demoNotice")}{" "}
|
||||||
own question, please{" "}
|
|
||||||
<a
|
<a
|
||||||
className="underline"
|
className="underline"
|
||||||
href="https://github.com/bytedance/deer-flow"
|
href="https://github.com/bytedance/deer-flow"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
click here
|
{t("clickHere")}
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
to clone it locally and run it.
|
{t("cloneLocally")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { PythonOutlined } from "@ant-design/icons";
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { LRUCache } from "lru-cache";
|
import { LRUCache } from "lru-cache";
|
||||||
import { BookOpenText, FileText, PencilRuler, Search } from "lucide-react";
|
import { BookOpenText, FileText, PencilRuler, Search } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||||
@@ -122,6 +123,7 @@ type SearchResult =
|
|||||||
};
|
};
|
||||||
|
|
||||||
function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||||
|
const t = useTranslations("chat.research");
|
||||||
const searching = useMemo(() => {
|
const searching = useMemo(() => {
|
||||||
return toolCall.result === undefined;
|
return toolCall.result === undefined;
|
||||||
}, [toolCall.result]);
|
}, [toolCall.result]);
|
||||||
@@ -159,7 +161,7 @@ function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
|||||||
animated={searchResults === undefined}
|
animated={searchResults === undefined}
|
||||||
>
|
>
|
||||||
<Search size={16} className={"mr-2"} />
|
<Search size={16} className={"mr-2"} />
|
||||||
<span>Searching for </span>
|
<span>{t("searchingFor")} </span>
|
||||||
<span className="max-w-[500px] overflow-hidden text-ellipsis whitespace-nowrap">
|
<span className="max-w-[500px] overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
{(toolCall.args as { query: string }).query}
|
{(toolCall.args as { query: string }).query}
|
||||||
</span>
|
</span>
|
||||||
@@ -238,6 +240,7 @@ function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CrawlToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
function CrawlToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||||
|
const t = useTranslations("chat.research");
|
||||||
const url = useMemo(
|
const url = useMemo(
|
||||||
() => (toolCall.args as { url: string }).url,
|
() => (toolCall.args as { url: string }).url,
|
||||||
[toolCall.args],
|
[toolCall.args],
|
||||||
@@ -251,7 +254,7 @@ function CrawlToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
|||||||
animated={toolCall.result === undefined}
|
animated={toolCall.result === undefined}
|
||||||
>
|
>
|
||||||
<BookOpenText size={16} className={"mr-2"} />
|
<BookOpenText size={16} className={"mr-2"} />
|
||||||
<span>Reading</span>
|
<span>{t("reading")}</span>
|
||||||
</RainbowText>
|
</RainbowText>
|
||||||
</div>
|
</div>
|
||||||
<ul className="mt-2 flex flex-wrap gap-4">
|
<ul className="mt-2 flex flex-wrap gap-4">
|
||||||
@@ -279,6 +282,7 @@ function CrawlToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function RetrieverToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
function RetrieverToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||||
|
const t = useTranslations("chat.research");
|
||||||
const searching = useMemo(() => {
|
const searching = useMemo(() => {
|
||||||
return toolCall.result === undefined;
|
return toolCall.result === undefined;
|
||||||
}, [toolCall.result]);
|
}, [toolCall.result]);
|
||||||
@@ -292,7 +296,7 @@ function RetrieverToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
|||||||
<div className="font-medium italic">
|
<div className="font-medium italic">
|
||||||
<RainbowText className="flex items-center" animated={searching}>
|
<RainbowText className="flex items-center" animated={searching}>
|
||||||
<Search size={16} className={"mr-2"} />
|
<Search size={16} className={"mr-2"} />
|
||||||
<span>Retrieving documents from RAG </span>
|
<span>{t("retrievingDocuments")} </span>
|
||||||
<span className="max-w-[500px] overflow-hidden text-ellipsis whitespace-nowrap">
|
<span className="max-w-[500px] overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
{(toolCall.args as { keywords: string }).keywords}
|
{(toolCall.args as { keywords: string }).keywords}
|
||||||
</span>
|
</span>
|
||||||
@@ -337,6 +341,7 @@ function RetrieverToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||||
|
const t = useTranslations("chat.research");
|
||||||
const code = useMemo<string | undefined>(() => {
|
const code = useMemo<string | undefined>(() => {
|
||||||
return (toolCall.args as { code?: string }).code;
|
return (toolCall.args as { code?: string }).code;
|
||||||
}, [toolCall.args]);
|
}, [toolCall.args]);
|
||||||
@@ -349,7 +354,7 @@ function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
|||||||
className="text-base font-medium italic"
|
className="text-base font-medium italic"
|
||||||
animated={toolCall.result === undefined}
|
animated={toolCall.result === undefined}
|
||||||
>
|
>
|
||||||
Running Python code
|
{t("runningPythonCode")}
|
||||||
</RainbowText>
|
</RainbowText>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -373,6 +378,7 @@ function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PythonToolCallResult({ result }: { result: string }) {
|
function PythonToolCallResult({ result }: { result: string }) {
|
||||||
|
const t = useTranslations("chat.research");
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
const hasError = useMemo(
|
const hasError = useMemo(
|
||||||
() => result.includes("Error executing code:\n"),
|
() => result.includes("Error executing code:\n"),
|
||||||
@@ -399,7 +405,7 @@ function PythonToolCallResult({ result }: { result: string }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mt-4 font-medium italic">
|
<div className="mt-4 font-medium italic">
|
||||||
{hasError ? "Error when executing the above code" : "Execution output"}
|
{hasError ? t("errorExecutingCode") : t("executionOutput")}
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-accent mt-2 max-h-[400px] max-w-[calc(100%-120px)] overflow-y-auto rounded-md p-2 text-sm">
|
<div className="bg-accent mt-2 max-h-[400px] max-w-[calc(100%-120px)] overflow-y-auto rounded-md p-2 text-sm">
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { Check, Copy, Headphones, Pencil, Undo2, X, Download } from "lucide-react";
|
import { Check, Copy, Headphones, Pencil, Undo2, X, Download } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { ScrollContainer } from "~/components/deer-flow/scroll-container";
|
import { ScrollContainer } from "~/components/deer-flow/scroll-container";
|
||||||
@@ -23,6 +24,7 @@ export function ResearchBlock({
|
|||||||
className?: string;
|
className?: string;
|
||||||
researchId: string | null;
|
researchId: string | null;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("chat.research");
|
||||||
const reportId = useStore((state) =>
|
const reportId = useStore((state) =>
|
||||||
researchId ? state.researchReportIds.get(researchId) : undefined,
|
researchId ? state.researchReportIds.get(researchId) : undefined,
|
||||||
);
|
);
|
||||||
@@ -108,7 +110,7 @@ export function ResearchBlock({
|
|||||||
<div className="absolute right-4 flex h-9 items-center justify-center">
|
<div className="absolute right-4 flex h-9 items-center justify-center">
|
||||||
{hasReport && !reportStreaming && (
|
{hasReport && !reportStreaming && (
|
||||||
<>
|
<>
|
||||||
<Tooltip title="Generate podcast">
|
<Tooltip title={t("generatePodcast")}>
|
||||||
<Button
|
<Button
|
||||||
className="text-gray-400"
|
className="text-gray-400"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -119,7 +121,7 @@ export function ResearchBlock({
|
|||||||
<Headphones />
|
<Headphones />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Edit">
|
<Tooltip title={t("edit")}>
|
||||||
<Button
|
<Button
|
||||||
className="text-gray-400"
|
className="text-gray-400"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -130,7 +132,7 @@ export function ResearchBlock({
|
|||||||
{editing ? <Undo2 /> : <Pencil />}
|
{editing ? <Undo2 /> : <Pencil />}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Copy">
|
<Tooltip title={t("copy")}>
|
||||||
<Button
|
<Button
|
||||||
className="text-gray-400"
|
className="text-gray-400"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -140,7 +142,7 @@ export function ResearchBlock({
|
|||||||
{copied ? <Check /> : <Copy />}
|
{copied ? <Check /> : <Copy />}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Download report as markdown">
|
<Tooltip title={t("downloadReport")}>
|
||||||
<Button
|
<Button
|
||||||
className="text-gray-400"
|
className="text-gray-400"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -152,7 +154,7 @@ export function ResearchBlock({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Tooltip title="Close">
|
<Tooltip title={t("close")}>
|
||||||
<Button
|
<Button
|
||||||
className="text-gray-400"
|
className="text-gray-400"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -177,10 +179,10 @@ export function ResearchBlock({
|
|||||||
value="report"
|
value="report"
|
||||||
disabled={!hasReport}
|
disabled={!hasReport}
|
||||||
>
|
>
|
||||||
Report
|
{t("report")}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger className="px-8" value="activities">
|
<TabsTrigger className="px-8" value="activities">
|
||||||
Activities
|
{t("activities")}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,12 +3,16 @@
|
|||||||
|
|
||||||
import { StarFilledIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
|
import { StarFilledIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
import { LanguageSwitcher } from "~/components/deer-flow/language-switcher";
|
||||||
import { NumberTicker } from "~/components/magicui/number-ticker";
|
import { NumberTicker } from "~/components/magicui/number-ticker";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
|
|
||||||
export async function SiteHeader() {
|
export function SiteHeader() {
|
||||||
|
const t = useTranslations('common');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="supports-backdrop-blur:bg-background/80 bg-background/40 sticky top-0 left-0 z-40 flex h-15 w-full flex-col items-center backdrop-blur-lg">
|
<header className="supports-backdrop-blur:bg-background/80 bg-background/40 sticky top-0 left-0 z-40 flex h-15 w-full flex-col items-center backdrop-blur-lg">
|
||||||
<div className="container flex h-15 items-center justify-between px-3">
|
<div className="container flex h-15 items-center justify-between px-3">
|
||||||
@@ -16,7 +20,8 @@ export async function SiteHeader() {
|
|||||||
<span className="mr-1 text-2xl">🦌</span>
|
<span className="mr-1 text-2xl">🦌</span>
|
||||||
<span>DeerFlow</span>
|
<span>DeerFlow</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex items-center">
|
<div className="relative flex items-center gap-2">
|
||||||
|
<LanguageSwitcher />
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none absolute inset-0 z-0 h-full w-full rounded-full opacity-60 blur-2xl"
|
className="pointer-events-none absolute inset-0 z-0 h-full w-full rounded-full opacity-60 blur-2xl"
|
||||||
style={{
|
style={{
|
||||||
@@ -32,7 +37,7 @@ export async function SiteHeader() {
|
|||||||
>
|
>
|
||||||
<Link href="https://github.com/bytedance/deer-flow" target="_blank">
|
<Link href="https://github.com/bytedance/deer-flow" target="_blank">
|
||||||
<GitHubLogoIcon className="size-4" />
|
<GitHubLogoIcon className="size-4" />
|
||||||
Star on GitHub
|
{t('starOnGitHub')}
|
||||||
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY &&
|
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY &&
|
||||||
env.GITHUB_OAUTH_TOKEN && <StarCounter />}
|
env.GITHUB_OAUTH_TOKEN && <StarCounter />}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
export function Welcome({ className }: { className?: string }) {
|
export function Welcome({ className }: { className?: string }) {
|
||||||
|
const t = useTranslations("chat.welcome");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className={cn("flex flex-col", className)}
|
className={cn("flex flex-col", className)}
|
||||||
@@ -13,21 +16,9 @@ export function Welcome({ className }: { className?: string }) {
|
|||||||
initial={{ opacity: 0, scale: 0.85 }}
|
initial={{ opacity: 0, scale: 0.85 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
>
|
>
|
||||||
<h3 className="mb-2 text-center text-3xl font-medium">
|
<h3 className="mb-2 text-center text-3xl font-medium">{t("greeting")}</h3>
|
||||||
👋 Hello, there!
|
|
||||||
</h3>
|
|
||||||
<div className="text-muted-foreground px-4 text-center text-lg">
|
<div className="text-muted-foreground px-4 text-center text-lg">
|
||||||
Welcome to{" "}
|
{t("description")}
|
||||||
<a
|
|
||||||
href="https://github.com/bytedance/deer-flow"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="hover:underline"
|
|
||||||
>
|
|
||||||
🦌 DeerFlow
|
|
||||||
</a>
|
|
||||||
, a deep research assistant built on cutting-edge language models, helps
|
|
||||||
you search on web, browse information, and handle complex tasks.
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { GithubOutlined } from "@ant-design/icons";
|
import { GithubOutlined } from "@ant-design/icons";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
|
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
@@ -25,12 +26,14 @@ const Main = dynamic(() => import("./main"), {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
|
const t = useTranslations("chat.page");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen justify-center overscroll-none">
|
<div className="flex h-screen w-screen justify-center overscroll-none">
|
||||||
<header className="fixed top-0 left-0 flex h-12 w-full items-center justify-between px-4">
|
<header className="fixed top-0 left-0 flex h-12 w-full items-center justify-between px-4">
|
||||||
<Logo />
|
<Logo />
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Tooltip title="Star DeerFlow on GitHub">
|
<Tooltip title={t("starOnGitHub")}>
|
||||||
<Button variant="ghost" size="icon" asChild>
|
<Button variant="ghost" size="icon" asChild>
|
||||||
<Link
|
<Link
|
||||||
href="https://github.com/bytedance/deer-flow"
|
href="https://github.com/bytedance/deer-flow"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { GithubFilled } from "@ant-design/icons";
|
import { GithubFilled } from "@ant-design/icons";
|
||||||
import { ChevronRight } from "lucide-react";
|
import { ChevronRight } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
import { AuroraText } from "~/components/magicui/aurora-text";
|
import { AuroraText } from "~/components/magicui/aurora-text";
|
||||||
import { FlickeringGrid } from "~/components/magicui/flickering-grid";
|
import { FlickeringGrid } from "~/components/magicui/flickering-grid";
|
||||||
@@ -11,6 +12,9 @@ import { Button } from "~/components/ui/button";
|
|||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
|
|
||||||
export function Jumbotron() {
|
export function Jumbotron() {
|
||||||
|
const t = useTranslations('hero');
|
||||||
|
const tCommon = useTranslations('common');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="flex h-[95vh] w-full flex-col items-center justify-center pb-15">
|
<section className="flex h-[95vh] w-full flex-col items-center justify-center pb-15">
|
||||||
<FlickeringGrid
|
<FlickeringGrid
|
||||||
@@ -34,15 +38,12 @@ export function Jumbotron() {
|
|||||||
<div className="relative z-10 flex flex-col items-center justify-center gap-12">
|
<div className="relative z-10 flex flex-col items-center justify-center gap-12">
|
||||||
<h1 className="text-center text-4xl font-bold md:text-6xl">
|
<h1 className="text-center text-4xl font-bold md:text-6xl">
|
||||||
<span className="bg-gradient-to-r from-white via-gray-200 to-gray-400 bg-clip-text text-transparent">
|
<span className="bg-gradient-to-r from-white via-gray-200 to-gray-400 bg-clip-text text-transparent">
|
||||||
Deep Research{" "}
|
{t('title')}{" "}
|
||||||
</span>
|
</span>
|
||||||
<AuroraText>at Your Fingertips</AuroraText>
|
<AuroraText>{t('subtitle')}</AuroraText>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="max-w-4xl p-2 text-center text-sm opacity-85 md:text-2xl">
|
<p className="max-w-4xl p-2 text-center text-sm opacity-85 md:text-2xl">
|
||||||
Meet DeerFlow, your personal Deep Research assistant. With powerful
|
{t('description')}
|
||||||
tools like search engines, web crawlers, Python and MCP services, it
|
|
||||||
delivers instant insights, comprehensive reports, and even captivating
|
|
||||||
podcasts.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-6">
|
<div className="flex gap-6">
|
||||||
<Button className="hidden text-lg md:flex md:w-42" size="lg" asChild>
|
<Button className="hidden text-lg md:flex md:w-42" size="lg" asChild>
|
||||||
@@ -56,7 +57,7 @@ export function Jumbotron() {
|
|||||||
: "/chat"
|
: "/chat"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Get Started <ChevronRight />
|
{tCommon('getStarted')} <ChevronRight />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
{!env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY && (
|
{!env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY && (
|
||||||
@@ -71,14 +72,14 @@ export function Jumbotron() {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<GithubFilled />
|
<GithubFilled />
|
||||||
Learn More
|
{tCommon('learnMore')}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-8 flex text-xs opacity-50">
|
<div className="absolute bottom-8 flex text-xs opacity-50">
|
||||||
<p>* DEER stands for Deep Exploration and Efficient Research.</p>
|
<p>{t('footnote')}</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
Minimize,
|
Minimize,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
|
||||||
import { Tooltip } from "~/components/deer-flow/tooltip";
|
import { Tooltip } from "~/components/deer-flow/tooltip";
|
||||||
@@ -47,6 +48,7 @@ const nodeTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function MultiAgentVisualization({ className }: { className?: string }) {
|
export function MultiAgentVisualization({ className }: { className?: string }) {
|
||||||
|
const t = useTranslations("chat.multiAgent");
|
||||||
const {
|
const {
|
||||||
graph: { nodes, edges },
|
graph: { nodes, edges },
|
||||||
activeStepIndex,
|
activeStepIndex,
|
||||||
@@ -120,12 +122,12 @@ export function MultiAgentVisualization({ className }: { className?: string }) {
|
|||||||
<div className="h-4 shrink-0"></div>
|
<div className="h-4 shrink-0"></div>
|
||||||
<div className="flex h-6 w-full shrink-0 items-center justify-center">
|
<div className="flex h-6 w-full shrink-0 items-center justify-center">
|
||||||
<div className="bg-muted/50 z-[200] flex rounded-3xl px-4 py-2">
|
<div className="bg-muted/50 z-[200] flex rounded-3xl px-4 py-2">
|
||||||
<Tooltip title="Move to the previous step">
|
<Tooltip title={t("moveToPrevious")}>
|
||||||
<Button variant="ghost" onClick={prevStep}>
|
<Button variant="ghost" onClick={prevStep}>
|
||||||
<ChevronLeft className="size-5" />
|
<ChevronLeft className="size-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Play / Pause">
|
<Tooltip title={t("playPause")}>
|
||||||
<Button variant="ghost" onClick={togglePlay}>
|
<Button variant="ghost" onClick={togglePlay}>
|
||||||
{playing ? (
|
{playing ? (
|
||||||
<Pause className="size-5" />
|
<Pause className="size-5" />
|
||||||
@@ -134,7 +136,7 @@ export function MultiAgentVisualization({ className }: { className?: string }) {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Move to the next step">
|
<Tooltip title={t("moveToNext")}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -158,7 +160,7 @@ export function MultiAgentVisualization({ className }: { className?: string }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip title="Toggle fullscreen">
|
<Tooltip title={t("toggleFullscreen")}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|||||||
@@ -3,93 +3,52 @@
|
|||||||
|
|
||||||
import { Bike, Building, Film, Github, Ham, Home, Pizza } from "lucide-react";
|
import { Bike, Building, Film, Github, Ham, Home, Pizza } from "lucide-react";
|
||||||
import { Bot } from "lucide-react";
|
import { Bot } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { BentoCard } from "~/components/magicui/bento-grid";
|
import { BentoCard } from "~/components/magicui/bento-grid";
|
||||||
|
|
||||||
import { SectionHeader } from "../components/section-header";
|
import { SectionHeader } from "../components/section-header";
|
||||||
|
|
||||||
const caseStudies = [
|
const caseStudyIcons = [
|
||||||
{
|
{ id: "eiffel-tower-vs-tallest-building", icon: Building },
|
||||||
id: "eiffel-tower-vs-tallest-building",
|
{ id: "github-top-trending-repo", icon: Github },
|
||||||
icon: Building,
|
{ id: "nanjing-traditional-dishes", icon: Ham },
|
||||||
title: "How tall is Eiffel Tower compared to tallest building?",
|
{ id: "rental-apartment-decoration", icon: Home },
|
||||||
description:
|
{ id: "review-of-the-professional", icon: Film },
|
||||||
"The research compares the heights and global significance of the Eiffel Tower and Burj Khalifa, and uses Python code to calculate the multiples.",
|
{ id: "china-food-delivery", icon: Bike },
|
||||||
},
|
{ id: "ultra-processed-foods", icon: Pizza },
|
||||||
{
|
{ id: "ai-twin-insurance", icon: Bot },
|
||||||
id: "github-top-trending-repo",
|
|
||||||
icon: Github,
|
|
||||||
title: "What are the top trending repositories on GitHub?",
|
|
||||||
description:
|
|
||||||
"The research utilized MCP services to identify the most popular GitHub repositories and documented them in detail using search engines.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "nanjing-traditional-dishes",
|
|
||||||
icon: Ham,
|
|
||||||
title: "Write an article about Nanjing's traditional dishes",
|
|
||||||
description:
|
|
||||||
"The study vividly showcases Nanjing's famous dishes through rich content and imagery, uncovering their hidden histories and cultural significance.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "rental-apartment-decoration",
|
|
||||||
icon: Home,
|
|
||||||
title: "How to decorate a small rental apartment?",
|
|
||||||
description:
|
|
||||||
"The study provides readers with practical and straightforward methods for decorating apartments, accompanied by inspiring images.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "review-of-the-professional",
|
|
||||||
icon: Film,
|
|
||||||
title: "Introduce the movie 'Léon: The Professional'",
|
|
||||||
description:
|
|
||||||
"The research provides a comprehensive introduction to the movie 'Léon: The Professional', including its plot, characters, and themes.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "china-food-delivery",
|
|
||||||
icon: Bike,
|
|
||||||
title: "How do you view the takeaway war in China? (in Chinese)",
|
|
||||||
description:
|
|
||||||
"The research analyzes the intensifying competition between JD and Meituan, highlighting their strategies, technological innovations, and challenges.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "ultra-processed-foods",
|
|
||||||
icon: Pizza,
|
|
||||||
title: "Are ultra-processed foods linked to health?",
|
|
||||||
description:
|
|
||||||
"The research examines the health risks of rising ultra-processed food consumption, urging more research on long-term effects and individual differences.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "ai-twin-insurance",
|
|
||||||
icon: Bot,
|
|
||||||
title: 'Write an article on "Would you insure your AI twin?"',
|
|
||||||
description:
|
|
||||||
"The research explores the concept of insuring AI twins, highlighting their benefits, risks, ethical considerations, and the evolving regulatory.",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function CaseStudySection() {
|
export function CaseStudySection() {
|
||||||
|
const t = useTranslations("landing.caseStudies");
|
||||||
|
const cases = t.raw("cases") as Array<{ title: string; description: string }>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative container hidden flex-col items-center justify-center md:flex">
|
<section className="relative container hidden flex-col items-center justify-center md:flex">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
anchor="case-studies"
|
anchor="case-studies"
|
||||||
title="Case Studies"
|
title={t("title")}
|
||||||
description="See DeerFlow in action through replays."
|
description={t("description")}
|
||||||
/>
|
/>
|
||||||
<div className="grid w-3/4 grid-cols-1 gap-2 sm:w-full sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
<div className="grid w-3/4 grid-cols-1 gap-2 sm:w-full sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
{caseStudies.map((caseStudy) => (
|
{cases.map((caseStudy, index) => {
|
||||||
<div key={caseStudy.title} className="w-full p-2">
|
const iconData = caseStudyIcons[index];
|
||||||
<BentoCard
|
return (
|
||||||
{...{
|
<div key={caseStudy.title} className="w-full p-2">
|
||||||
Icon: caseStudy.icon,
|
<BentoCard
|
||||||
name: caseStudy.title,
|
{...{
|
||||||
description: caseStudy.description,
|
Icon: iconData?.icon ?? Building,
|
||||||
href: `/chat?replay=${caseStudy.id}`,
|
name: caseStudy.title,
|
||||||
cta: "Click to watch replay",
|
description: caseStudy.description,
|
||||||
className: "w-full h-full",
|
href: `/chat?replay=${iconData?.id}`,
|
||||||
}}
|
cta: t("clickToWatch"),
|
||||||
/>
|
className: "w-full h-full",
|
||||||
</div>
|
}}
|
||||||
))}
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,87 +1,90 @@
|
|||||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { Bird, Microscope, Podcast, Usb, User } from "lucide-react";
|
import {
|
||||||
|
Bird,
|
||||||
|
Microscope,
|
||||||
|
Podcast,
|
||||||
|
Usb,
|
||||||
|
User,
|
||||||
|
type LucideProps,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import type { ForwardRefExoticComponent, RefAttributes } from "react";
|
||||||
|
|
||||||
import { BentoCard, BentoGrid } from "~/components/magicui/bento-grid";
|
import { BentoCard, BentoGrid } from "~/components/magicui/bento-grid";
|
||||||
|
|
||||||
import { SectionHeader } from "../components/section-header";
|
import { SectionHeader } from "../components/section-header";
|
||||||
|
|
||||||
const features = [
|
type FeatureIcon = {
|
||||||
|
Icon: ForwardRefExoticComponent<
|
||||||
|
Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>
|
||||||
|
>;
|
||||||
|
href: string;
|
||||||
|
className: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const featureIcons: Array<FeatureIcon> = [
|
||||||
{
|
{
|
||||||
Icon: Microscope,
|
Icon: Microscope,
|
||||||
name: "Dive Deeper and Reach Wider",
|
|
||||||
description:
|
|
||||||
"Unlock deeper insights with advanced tools. Our powerful search + crawling and Python tools gathers comprehensive data, delivering in-depth reports to enhance your study.",
|
|
||||||
href: "https://github.com/bytedance/deer-flow/blob/main/src/tools",
|
href: "https://github.com/bytedance/deer-flow/blob/main/src/tools",
|
||||||
cta: "Learn more",
|
|
||||||
background: (
|
|
||||||
<img alt="background" className="absolute -top-20 -right-20 opacity-60" />
|
|
||||||
),
|
|
||||||
className: "lg:col-start-1 lg:col-end-2 lg:row-start-1 lg:row-end-3",
|
className: "lg:col-start-1 lg:col-end-2 lg:row-start-1 lg:row-end-3",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Icon: User,
|
Icon: User,
|
||||||
name: "Human-in-the-loop",
|
|
||||||
description:
|
|
||||||
"Refine your research plan, or adjust focus areas all through simple natural language.",
|
|
||||||
href: "https://github.com/bytedance/deer-flow/blob/main/src/graph/nodes.py",
|
href: "https://github.com/bytedance/deer-flow/blob/main/src/graph/nodes.py",
|
||||||
cta: "Learn more",
|
|
||||||
background: (
|
|
||||||
<img alt="background" className="absolute -top-20 -right-20 opacity-60" />
|
|
||||||
),
|
|
||||||
className: "lg:col-start-1 lg:col-end-2 lg:row-start-3 lg:row-end-4",
|
className: "lg:col-start-1 lg:col-end-2 lg:row-start-3 lg:row-end-4",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Icon: Bird,
|
Icon: Bird,
|
||||||
name: "Lang Stack",
|
|
||||||
description:
|
|
||||||
"Build with confidence using the LangChain and LangGraph frameworks.",
|
|
||||||
href: "https://www.langchain.com/",
|
href: "https://www.langchain.com/",
|
||||||
cta: "Learn more",
|
|
||||||
background: (
|
|
||||||
<img alt="background" className="absolute -top-20 -right-20 opacity-60" />
|
|
||||||
),
|
|
||||||
className: "lg:col-start-2 lg:col-end-3 lg:row-start-1 lg:row-end-2",
|
className: "lg:col-start-2 lg:col-end-3 lg:row-start-1 lg:row-end-2",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Icon: Usb,
|
Icon: Usb,
|
||||||
name: "MCP Integrations",
|
|
||||||
description:
|
|
||||||
"Supercharge your research workflow and expand your toolkit with seamless MCP integrations.",
|
|
||||||
href: "https://github.com/bytedance/deer-flow/blob/main/src/graph/nodes.py",
|
href: "https://github.com/bytedance/deer-flow/blob/main/src/graph/nodes.py",
|
||||||
cta: "Learn more",
|
|
||||||
background: (
|
|
||||||
<img alt="background" className="absolute -top-20 -right-20 opacity-60" />
|
|
||||||
),
|
|
||||||
className: "lg:col-start-2 lg:col-end-3 lg:row-start-2 lg:row-end-3",
|
className: "lg:col-start-2 lg:col-end-3 lg:row-start-2 lg:row-end-3",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Icon: Podcast,
|
Icon: Podcast,
|
||||||
name: "Podcast Generation",
|
|
||||||
description:
|
|
||||||
"Instantly generate podcasts from reports. Perfect for on-the-go learning or sharing findings effortlessly. ",
|
|
||||||
href: "https://github.com/bytedance/deer-flow/blob/main/src/podcast",
|
href: "https://github.com/bytedance/deer-flow/blob/main/src/podcast",
|
||||||
cta: "Learn more",
|
|
||||||
background: (
|
|
||||||
<img alt="background" className="absolute -top-20 -right-20 opacity-60" />
|
|
||||||
),
|
|
||||||
className: "lg:col-start-2 lg:col-end-3 lg:row-start-3 lg:row-end-4",
|
className: "lg:col-start-2 lg:col-end-3 lg:row-start-3 lg:row-end-4",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function CoreFeatureSection() {
|
export function CoreFeatureSection() {
|
||||||
|
const t = useTranslations("landing.coreFeatures");
|
||||||
|
const tCommon = useTranslations("common");
|
||||||
|
const features = t.raw("features") as Array<{
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative flex w-full flex-col content-around items-center justify-center">
|
<section className="relative flex w-full flex-col content-around items-center justify-center">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
anchor="core-features"
|
anchor="core-features"
|
||||||
title="Core Features"
|
title={t("title")}
|
||||||
description="Find out what makes DeerFlow effective."
|
description={t("description")}
|
||||||
/>
|
/>
|
||||||
<BentoGrid className="w-3/4 lg:grid-cols-2 lg:grid-rows-3">
|
<BentoGrid className="w-3/4 lg:grid-cols-2 lg:grid-rows-3">
|
||||||
{features.map((feature) => (
|
{features.map((feature, index) => {
|
||||||
<BentoCard key={feature.name} {...feature} />
|
const iconData = featureIcons[index];
|
||||||
))}
|
return iconData ? (
|
||||||
|
<BentoCard
|
||||||
|
key={feature.name}
|
||||||
|
{...iconData}
|
||||||
|
{...feature}
|
||||||
|
background={
|
||||||
|
<img
|
||||||
|
alt="background"
|
||||||
|
className="absolute -top-20 -right-20 opacity-60"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
cta={tCommon("learnMore")}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
</BentoGrid>
|
</BentoGrid>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import { GithubFilled } from "@ant-design/icons";
|
import { GithubFilled } from "@ant-design/icons";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { AuroraText } from "~/components/magicui/aurora-text";
|
import { AuroraText } from "~/components/magicui/aurora-text";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
@@ -10,21 +11,22 @@ import { Button } from "~/components/ui/button";
|
|||||||
import { SectionHeader } from "../components/section-header";
|
import { SectionHeader } from "../components/section-header";
|
||||||
|
|
||||||
export function JoinCommunitySection() {
|
export function JoinCommunitySection() {
|
||||||
|
const t = useTranslations("landing.joinCommunity");
|
||||||
return (
|
return (
|
||||||
<section className="flex w-full flex-col items-center justify-center pb-12">
|
<section className="flex w-full flex-col items-center justify-center pb-12">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
anchor="join-community"
|
anchor="join-community"
|
||||||
title={
|
title={
|
||||||
<AuroraText colors={["#60A5FA", "#A5FA60", "#A560FA"]}>
|
<AuroraText colors={["#60A5FA", "#A5FA60", "#A560FA"]}>
|
||||||
Join the DeerFlow Community
|
{t("title")}
|
||||||
</AuroraText>
|
</AuroraText>
|
||||||
}
|
}
|
||||||
description="Contribute brilliant ideas to shape the future of DeerFlow. Collaborate, innovate, and make impacts."
|
description={t("description")}
|
||||||
/>
|
/>
|
||||||
<Button className="text-xl" size="lg" asChild>
|
<Button className="text-xl" size="lg" asChild>
|
||||||
<Link href="https://github.com/bytedance/deer-flow" target="_blank">
|
<Link href="https://github.com/bytedance/deer-flow" target="_blank">
|
||||||
<GithubFilled />
|
<GithubFilled />
|
||||||
Contribute Now
|
{t("contributeNow")}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { MultiAgentVisualization } from "../components/multi-agent-visualization";
|
import { MultiAgentVisualization } from "../components/multi-agent-visualization";
|
||||||
import { SectionHeader } from "../components/section-header";
|
import { SectionHeader } from "../components/section-header";
|
||||||
|
|
||||||
export function MultiAgentSection() {
|
export function MultiAgentSection() {
|
||||||
|
const t = useTranslations("landing.multiAgent");
|
||||||
return (
|
return (
|
||||||
<section className="relative flex w-full flex-col items-center justify-center">
|
<section className="relative flex w-full flex-col items-center justify-center">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
anchor="multi-agent-architecture"
|
anchor="multi-agent-architecture"
|
||||||
title="Multi-Agent Architecture"
|
title={t("title")}
|
||||||
description="Experience the agent teamwork with our Supervisor + Handoffs design pattern."
|
description={t("description")}
|
||||||
/>
|
/>
|
||||||
<div className="flex h-[70vh] w-full flex-col items-center justify-center">
|
<div className="flex h-[70vh] w-full flex-col items-center justify-center">
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
|
|||||||
+10
-6
@@ -6,9 +6,10 @@ import "~/styles/globals.css";
|
|||||||
import { type Metadata } from "next";
|
import { type Metadata } from "next";
|
||||||
import { Geist } from "next/font/google";
|
import { Geist } from "next/font/google";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
|
import { NextIntlClientProvider } from 'next-intl';
|
||||||
|
import { getLocale, getMessages } from 'next-intl/server';
|
||||||
|
|
||||||
import { ThemeProviderWrapper } from "~/components/deer-flow/theme-provider-wrapper";
|
import { ThemeProviderWrapper } from "~/components/deer-flow/theme-provider-wrapper";
|
||||||
import { loadConfig } from "~/core/api/config";
|
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
|
|
||||||
import { Toaster } from "../components/deer-flow/toaster";
|
import { Toaster } from "../components/deer-flow/toaster";
|
||||||
@@ -28,11 +29,12 @@ const geist = Geist({
|
|||||||
export default async function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{ children: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
const conf = await loadConfig();
|
const locale = await getLocale();
|
||||||
|
const messages = await getMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" className={`${geist.variable}`} suppressHydrationWarning>
|
<html lang={locale} className={`${geist.variable}`} suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<script>{`window.__deerflowConfig = ${JSON.stringify(conf)}`}</script>
|
|
||||||
{/* Define isSpace function globally to fix markdown-it issues with Next.js + Turbopack
|
{/* Define isSpace function globally to fix markdown-it issues with Next.js + Turbopack
|
||||||
https://github.com/markdown-it/markdown-it/issues/1082#issuecomment-2749656365 */}
|
https://github.com/markdown-it/markdown-it/issues/1082#issuecomment-2749656365 */}
|
||||||
<Script id="markdown-it-fix" strategy="beforeInteractive">
|
<Script id="markdown-it-fix" strategy="beforeInteractive">
|
||||||
@@ -46,8 +48,10 @@ export default async function RootLayout({
|
|||||||
</Script>
|
</Script>
|
||||||
</head>
|
</head>
|
||||||
<body className="bg-app">
|
<body className="bg-app">
|
||||||
<ThemeProviderWrapper>{children}</ThemeProviderWrapper>
|
<NextIntlClientProvider messages={messages}>
|
||||||
<Toaster />
|
<ThemeProviderWrapper>{children}</ThemeProviderWrapper>
|
||||||
|
<Toaster />
|
||||||
|
</NextIntlClientProvider>
|
||||||
{
|
{
|
||||||
// NO USER BEHAVIOR TRACKING OR PRIVATE DATA COLLECTION BY DEFAULT
|
// NO USER BEHAVIOR TRACKING OR PRIVATE DATA COLLECTION BY DEFAULT
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { SiteHeader } from "./chat/components/site-header";
|
import { SiteHeader } from "./chat/components/site-header";
|
||||||
@@ -27,20 +28,20 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Footer() {
|
function Footer() {
|
||||||
|
const t = useTranslations('footer');
|
||||||
const year = useMemo(() => new Date().getFullYear(), []);
|
const year = useMemo(() => new Date().getFullYear(), []);
|
||||||
return (
|
return (
|
||||||
<footer className="container mt-32 flex flex-col items-center justify-center">
|
<footer className="container mt-32 flex flex-col items-center justify-center">
|
||||||
<hr className="from-border/0 via-border/70 to-border/0 m-0 h-px w-full border-none bg-gradient-to-r" />
|
<hr className="from-border/0 via-border/70 to-border/0 m-0 h-px w-full border-none bg-gradient-to-r" />
|
||||||
<div className="text-muted-foreground container flex h-20 flex-col items-center justify-center text-sm">
|
<div className="text-muted-foreground container flex h-20 flex-col items-center justify-center text-sm">
|
||||||
<p className="text-center font-serif text-lg md:text-xl">
|
<p className="text-center font-serif text-lg md:text-xl">
|
||||||
"Originated from Open Source, give back to Open Source."
|
"{t('quote')}"
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground container mb-8 flex flex-col items-center justify-center text-xs">
|
<div className="text-muted-foreground container mb-8 flex flex-col items-center justify-center text-xs">
|
||||||
<p>Licensed under MIT License</p>
|
<p>{t('license')}</p>
|
||||||
<p>© {year} DeerFlow</p>
|
<p>© {year} {t('copyright')}</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useCallback, useState } from "react";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -29,11 +30,14 @@ export function AddMCPServerDialog({
|
|||||||
}: {
|
}: {
|
||||||
onAdd?: (servers: MCPServerMetadata[]) => void;
|
onAdd?: (servers: MCPServerMetadata[]) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("settings");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const [validationError, setValidationError] = useState<string | null>("");
|
const [validationError, setValidationError] = useState<string | null>("");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
const handleChange = useCallback((value: string) => {
|
const handleChange = useCallback((value: string) => {
|
||||||
setInput(value);
|
setInput(value);
|
||||||
if (!value.trim()) {
|
if (!value.trim()) {
|
||||||
@@ -48,7 +52,7 @@ export function AddMCPServerDialog({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setValidationError("Invalid JSON");
|
setValidationError(t("invalidJson"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = MCPConfigSchema.safeParse(JSON.parse(value));
|
const result = MCPConfigSchema.safeParse(JSON.parse(value));
|
||||||
@@ -63,18 +67,20 @@ export function AddMCPServerDialog({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
result.error.errors[0]?.message ?? "Validation failed";
|
result.error.errors[0]?.message ?? t("validationFailed");
|
||||||
setValidationError(errorMessage);
|
setValidationError(errorMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = Object.keys(result.data.mcpServers);
|
const keys = Object.keys(result.data.mcpServers);
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
setValidationError("Missing server name in `mcpServers`");
|
setValidationError(t("missingServerName"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}, []);
|
}, [t]);
|
||||||
|
|
||||||
const handleAdd = useCallback(async () => {
|
const handleAdd = useCallback(async () => {
|
||||||
|
abortControllerRef.current = new AbortController();
|
||||||
const config = MCPConfigSchema.parse(JSON.parse(input));
|
const config = MCPConfigSchema.parse(JSON.parse(input));
|
||||||
setInput(JSON.stringify(config, null, 2));
|
setInput(JSON.stringify(config, null, 2));
|
||||||
const addingServers: SimpleMCPServerMetadata[] = [];
|
const addingServers: SimpleMCPServerMetadata[] = [];
|
||||||
@@ -105,7 +111,7 @@ export function AddMCPServerDialog({
|
|||||||
setError(null);
|
setError(null);
|
||||||
for (const server of addingServers) {
|
for (const server of addingServers) {
|
||||||
processingServer = server.name;
|
processingServer = server.name;
|
||||||
const metadata = await queryMCPServerMetadata(server);
|
const metadata = await queryMCPServerMetadata(server, abortControllerRef.current.signal);
|
||||||
results.push({ ...metadata, name: server.name, enabled: true });
|
results.push({ ...metadata, name: server.name, enabled: true });
|
||||||
}
|
}
|
||||||
if (results.length > 0) {
|
if (results.length > 0) {
|
||||||
@@ -115,30 +121,41 @@ export function AddMCPServerDialog({
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setError(`Failed to add server: ${processingServer}`);
|
if (e instanceof Error && e.name === 'AbortError') {
|
||||||
|
setError(`Request was cancelled`);
|
||||||
|
} else {
|
||||||
|
setError(`Failed to add server: ${processingServer}`);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
|
abortControllerRef.current = null;
|
||||||
}
|
}
|
||||||
}, [input, onAdd]);
|
}, [input, onAdd]);
|
||||||
|
|
||||||
|
const handleAbort = () => {
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button size="sm">Add Servers</Button>
|
<Button size="sm">{t("addServers")}</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-[560px]">
|
<DialogContent className="sm:max-w-[560px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add New MCP Servers</DialogTitle>
|
<DialogTitle>{t("addNewMCPServers")}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
DeerFlow uses the standard JSON MCP config to create a new server.
|
{t("mcpConfigDescription")}
|
||||||
<br />
|
<br />
|
||||||
Paste your config below and click "Add" to add new servers.
|
{t("pasteConfigBelow")}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="h-[360px]"
|
className="h-[360px] break-all sm:max-w-[510px]"
|
||||||
placeholder={
|
placeholder={
|
||||||
'Example:\n\n{\n "mcpServers": {\n "My Server": {\n "command": "python",\n "args": [\n "-m", "mcp_server"\n ],\n "env": {\n "API_KEY": "YOUR_API_KEY"\n }\n }\n }\n}'
|
'Example:\n\n{\n "mcpServers": {\n "My Server": {\n "command": "python",\n "args": [\n "-m", "mcp_server"\n ],\n "env": {\n "API_KEY": "YOUR_API_KEY"\n }\n }\n }\n}'
|
||||||
}
|
}
|
||||||
@@ -154,7 +171,7 @@ export function AddMCPServerDialog({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||||
Cancel
|
{t("cancel", { defaultValue: "Cancel" })}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="w-24"
|
className="w-24"
|
||||||
@@ -163,8 +180,13 @@ export function AddMCPServerDialog({
|
|||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
>
|
>
|
||||||
{processing && <Loader2 className="animate-spin" />}
|
{processing && <Loader2 className="animate-spin" />}
|
||||||
Add
|
{t("add")}
|
||||||
</Button>
|
</Button>
|
||||||
|
{
|
||||||
|
processing && (
|
||||||
|
<Button variant="destructive" onClick={handleAbort}>Abort</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
import { Settings } from "lucide-react";
|
import { Settings } from "lucide-react";
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { Tooltip } from "~/components/deer-flow/tooltip";
|
import { Tooltip } from "~/components/deer-flow/tooltip";
|
||||||
@@ -29,6 +30,8 @@ import { cn } from "~/lib/utils";
|
|||||||
import { SETTINGS_TABS } from "../tabs";
|
import { SETTINGS_TABS } from "../tabs";
|
||||||
|
|
||||||
export function SettingsDialog() {
|
export function SettingsDialog() {
|
||||||
|
const t = useTranslations('settings');
|
||||||
|
const tCommon = useTranslations('common');
|
||||||
const { isReplay } = useReplay();
|
const { isReplay } = useReplay();
|
||||||
const [activeTabId, setActiveTabId] = useState(SETTINGS_TABS[0]!.id);
|
const [activeTabId, setActiveTabId] = useState(SETTINGS_TABS[0]!.id);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -92,7 +95,7 @@ export function SettingsDialog() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<Tooltip title="Settings">
|
<Tooltip title={tCommon('settings')}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
<Settings />
|
<Settings />
|
||||||
@@ -101,9 +104,9 @@ export function SettingsDialog() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<DialogContent className="sm:max-w-[850px]">
|
<DialogContent className="sm:max-w-[850px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>DeerFlow Settings</DialogTitle>
|
<DialogTitle>{t('title')}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Manage your DeerFlow settings here.
|
{t('description')}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Tabs value={activeTabId}>
|
<Tabs value={activeTabId}>
|
||||||
@@ -157,10 +160,10 @@ export function SettingsDialog() {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||||
Cancel
|
{tCommon('cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="w-24" type="submit" onClick={handleSave}>
|
<Button className="w-24" type="submit" onClick={handleSave}>
|
||||||
Save
|
{tCommon('save')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user