Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52ebd048f0 | |||
| b210c10304 | |||
| e88274d5b8 | |||
| 97ce9952a1 | |||
| cc7247d02a | |||
| d00682305f | |||
| 87b15168ed | |||
| a0d458c8eb | |||
| 3e364c1afe | |||
| 2ffacd9294 | |||
| 6693d844a7 | |||
| 8e68c13951 | |||
| c187ae511d | |||
| 397ac57235 | |||
| 447e427fd3 | |||
| eeff1ebf80 | |||
| 1cd6aa0ece | |||
| 8081a14c21 | |||
| c6ed423021 | |||
| 0e22c373af | |||
| cda3870add | |||
| b5ec61bb9d | |||
| 73ac8ae45a | |||
| 91648c4210 | |||
| 95257800d2 | |||
| 45568ca95b | |||
| db3e74629f | |||
| 0da52d41a7 | |||
| eaaad27e44 | |||
| 4ddd659d8d | |||
| 7e9fbed918 | |||
| fcbc7f1118 | |||
| d14fb262ea | |||
| 9888098f8a | |||
| 56e35c6b7f | |||
| 462752b462 | |||
| 0565ab6d27 | |||
| 29be360954 | |||
| 3ed70e11d5 | |||
| 55ce399969 | |||
| 8bbcdbe4de |
@@ -13,6 +13,12 @@ TAVILY_API_KEY=tvly-xxx
|
||||
# BRAVE_SEARCH_API_KEY=xxx # Required only if SEARCH_API is brave_search
|
||||
# JINA_API_KEY=jina_xxx # Optional, default is None
|
||||
|
||||
# Optional, RAG provider
|
||||
# RAG_PROVIDER=ragflow
|
||||
# RAGFLOW_API_URL="http://localhost:9388"
|
||||
# RAGFLOW_API_KEY="ragflow-xxx"
|
||||
# RAGFLOW_RETRIEVAL_SIZE=10
|
||||
|
||||
# Optional, volcengine TTS for generating podcast
|
||||
VOLCENGINE_TTS_APPID=xxx
|
||||
VOLCENGINE_TTS_ACCESS_TOKEN=xxx
|
||||
|
||||
@@ -6,6 +6,9 @@ on:
|
||||
pull_request:
|
||||
branches: [ '*' ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -6,6 +6,9 @@ on:
|
||||
pull_request:
|
||||
branches: [ '*' ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -6,11 +6,13 @@ dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
.coverage
|
||||
.coverage.*
|
||||
agent_history.gif
|
||||
static/browser_history/*.gif
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
venv/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
@@ -12,6 +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.
|
||||
|
||||
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.
|
||||
|
||||
Please visit [our official website](https://deerflow.tech/) for more details.
|
||||
|
||||
## Demo
|
||||
@@ -189,6 +191,18 @@ SEARCH_API=tavily
|
||||
- Crawling with Jina
|
||||
- Advanced content extraction
|
||||
|
||||
- 📃 **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/).
|
||||
|
||||
```bash
|
||||
# .env
|
||||
RAG_PROVIDER=ragflow
|
||||
RAGFLOW_API_URL="http://localhost:9388"
|
||||
RAGFLOW_API_KEY="ragflow-xxx"
|
||||
RAGFLOW_RETRIEVAL_SIZE=10
|
||||
```
|
||||
|
||||
- 🔗 **MCP Seamless Integration**
|
||||
- Expand capabilities for private domain access, knowledge graph, web browsing and more
|
||||
- Facilitates integration of diverse research tools and methodologies
|
||||
@@ -352,6 +366,7 @@ When you submit a research topic in the Studio UI, you'll be able to see the ent
|
||||
DeerFlow supports LangSmith tracing to help you debug and monitor your workflows. To enable LangSmith tracing:
|
||||
|
||||
1. Make sure your `.env` file has the following configurations (see `.env.example`):
|
||||
|
||||
```bash
|
||||
LANGSMITH_TRACING=true
|
||||
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
|
||||
@@ -538,6 +553,8 @@ We would like to extend our sincere appreciation to the following projects for t
|
||||
|
||||
- **[LangChain](https://github.com/langchain-ai/langchain)**: Their exceptional framework powers our LLM interactions and chains, enabling seamless integration and functionality.
|
||||
- **[LangGraph](https://github.com/langchain-ai/langgraph)**: Their innovative approach to multi-agent orchestration has been instrumental in enabling DeerFlow's sophisticated workflows.
|
||||
- **[Novel](https://github.com/steven-tey/novel)**: Their Notion-style WYSIWYG editor supports our report editing and AI-assisted rewriting.
|
||||
- **[RAGFlow](https://github.com/infiniflow/ragflow)**: We have achieved support for research on users' private knowledge bases through integration with RAGFlow.
|
||||
|
||||
These projects exemplify the transformative power of open-source collaboration, and we are proud to build upon their foundations.
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
**DeerFlow** (**D**eep **E**xploration and **E**fficient **R**esearch **Flow**) ist ein Community-getriebenes Framework für tiefgehende Recherche, das auf der großartigen Arbeit der Open-Source-Community aufbaut. Unser Ziel ist es, Sprachmodelle mit spezialisierten Werkzeugen für Aufgaben wie Websuche, Crawling und Python-Code-Ausführung zu kombinieren und gleichzeitig der Community, die dies möglich gemacht hat, etwas zurückzugeben.
|
||||
|
||||
Derzeit ist DeerFlow offiziell in das FaaS-Anwendungszentrum von Volcengine eingezogen. Benutzer können es über den Erfahrungslink online erleben, um seine leistungsstarken Funktionen und bequemen Operationen intuitiv zu spüren. Gleichzeitig unterstützt DeerFlow zur Erfüllung der Bereitstellungsanforderungen verschiedener Benutzer die Ein-Klick-Bereitstellung basierend auf Volcengine. Klicken Sie auf den Bereitstellungslink, um den Bereitstellungsprozess schnell abzuschließen und eine effiziente Forschungsreise zu beginnen.
|
||||
|
||||
Besuchen Sie [unsere offizielle Website](https://deerflow.tech/) für weitere Details.
|
||||
|
||||
## Demo
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
**DeerFlow** (**D**eep **E**xploration and **E**fficient **R**esearch **Flow**) es un marco de Investigación Profunda impulsado por la comunidad que se basa en el increíble trabajo de la comunidad de código abierto. Nuestro objetivo es combinar modelos de lenguaje con herramientas especializadas para tareas como búsqueda web, rastreo y ejecución de código Python, mientras devolvemos a la comunidad que hizo esto posible.
|
||||
|
||||
Actualmente, DeerFlow ha ingresado oficialmente al Centro de Aplicaciones FaaS de Volcengine. Los usuarios pueden experimentarlo en línea a través del enlace de experiencia para sentir intuitivamente sus potentes funciones y operaciones convenientes. Al mismo tiempo, para satisfacer las necesidades de implementación de diferentes usuarios, DeerFlow admite la implementación con un clic basada en Volcengine. Haga clic en el enlace de implementación para completar rápidamente el proceso de implementación y comenzar un viaje de investigación eficiente.
|
||||
|
||||
Por favor, visita [nuestra página web oficial](https://deerflow.tech/) para más detalles.
|
||||
|
||||
## Demostración
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
**DeerFlow**(**D**eep **E**xploration and **E**fficient **R**esearch **Flow**)は、オープンソースコミュニティの素晴らしい成果の上に構築されたコミュニティ主導の深層研究フレームワークです。私たちの目標は、言語モデルとウェブ検索、クローリング、Python コード実行などの専門ツールを組み合わせながら、これを可能にしたコミュニティに貢献することです。
|
||||
|
||||
現在、DeerFlow は火山引擎の FaaS アプリケーションセンターに正式に入居しています。ユーザーは体験リンクを通じてオンラインで体験し、その強力な機能と便利な操作を直感的に感じることができます。同時に、さまざまなユーザーの展開ニーズを満たすため、DeerFlow は火山引擎に基づくワンクリック展開をサポートしています。展開リンクをクリックして展開プロセスを迅速に完了し、効率的な研究の旅を始めましょう。
|
||||
|
||||
詳細については[DeerFlow の公式ウェブサイト](https://deerflow.tech/)をご覧ください。
|
||||
|
||||
## デモ
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
|
||||
**DeerFlow** (**D**eep **E**xploration and **E**fficient **R**esearch **Flow**) é um framework de Pesquisa Profunda orientado-a-comunidade que baseia-se em um íncrivel trabalho da comunidade open source. Nosso objetivo é combinar modelos de linguagem com ferramentas especializadas para tarefas como busca na web, crawling, e execução de código Python, enquanto retribui com a comunidade que o tornou possível.
|
||||
|
||||
Atualmente, o DeerFlow entrou oficialmente no Centro de Aplicações FaaS da Volcengine. Os usuários podem experimentá-lo online através do link de experiência para sentir intuitivamente suas funções poderosas e operações convenientes. Ao mesmo tempo, para atender às necessidades de implantação de diferentes usuários, o DeerFlow suporta implantação com um clique baseada na Volcengine. Clique no link de implantação para completar rapidamente o processo de implantação e iniciar uma jornada de pesquisa eficiente.
|
||||
|
||||
Por favor, visite [Nosso Site Oficial](https://deerflow.tech/) para maiores detalhes.
|
||||
|
||||
## Demo
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
**DeerFlow** (**D**eep **E**xploration and **E**fficient **R**esearch **Flow**) - это фреймворк для глубокого исследования, разработанный сообществом и основанный на впечатляющей работе сообщества открытого кода. Наша цель - объединить языковые модели со специализированными инструментами для таких задач, как веб-поиск, сканирование и выполнение кода Python, одновременно возвращая пользу сообществу, которое сделало это возможным.
|
||||
|
||||
В настоящее время DeerFlow официально вошел в Центр приложений FaaS Volcengine. Пользователи могут испытать его онлайн через ссылку для опыта, чтобы интуитивно почувствовать его мощные функции и удобные операции. В то же время, для удовлетворения потребностей развертывания различных пользователей, DeerFlow поддерживает развертывание одним кликом на основе Volcengine. Нажмите на ссылку развертывания, чтобы быстро завершить процесс развертывания и начать эффективное исследовательское путешествие.
|
||||
|
||||
Пожалуйста, посетите [наш официальный сайт](https://deerflow.tech/) для получения дополнительной информации.
|
||||
|
||||
## Демонстрация
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
**DeerFlow**(**D**eep **E**xploration and **E**fficient **R**esearch **Flow**)是一个社区驱动的深度研究框架,它建立在开源社区的杰出工作基础之上。我们的目标是将语言模型与专业工具(如网络搜索、爬虫和 Python 代码执行)相结合,同时回馈使这一切成为可能的社区。
|
||||
|
||||
目前,DeerFlow 已正式入驻火山引擎的 FaaS 应用中心,用户可通过体验链接进行在线体验,直观感受其强大功能与便捷操作;同时,为满足不同用户的部署需求,DeerFlow 支持基于火山引擎一键部署,点击部署链接即可快速完成部署流程,开启高效研究之旅。
|
||||
|
||||
请访问[DeerFlow 的官方网站](https://deerflow.tech/)了解更多详情。
|
||||
|
||||
## 演示
|
||||
|
||||
+14
-3
@@ -1,9 +1,20 @@
|
||||
# [!NOTE]
|
||||
# Read the `docs/configuration_guide.md` carefully, and update the configurations to match your specific settings and requirements.
|
||||
# - Replace `api_key` with your own credentials
|
||||
# - Replace `base_url` and `model` name if you want to use a custom model
|
||||
# Read the `docs/configuration_guide.md` carefully, and update the
|
||||
# configurations to match your specific settings and requirements.
|
||||
# - Replace `api_key` with your own credentials.
|
||||
# - Replace `base_url` and `model` name if you want to use a custom model.
|
||||
# - A restart is required every time you change the `config.yaml` file.
|
||||
|
||||
BASIC_MODEL:
|
||||
base_url: https://ark.cn-beijing.volces.com/api/v3
|
||||
model: "doubao-1-5-pro-32k-250115"
|
||||
api_key: xxxx
|
||||
|
||||
# Reasoning model is optional.
|
||||
# Uncomment the following settings if you want to use reasoning model
|
||||
# for planning.
|
||||
|
||||
# REASONING_MODEL:
|
||||
# base_url: https://ark-cn-beijing.bytedance.net/api/v3
|
||||
# model: "doubao-1-5-thinking-pro-m-250428"
|
||||
# api_key: xxxx
|
||||
|
||||
@@ -49,7 +49,7 @@ BASIC_MODEL:
|
||||
BASIC_MODEL:
|
||||
base_url: "https://api.deepseek.com"
|
||||
model: "deepseek-chat"
|
||||
api_key: YOU_API_KEY
|
||||
api_key: YOUR_API_KEY
|
||||
|
||||
# An example of Google Gemini models using OpenAI-Compatible interface
|
||||
BASIC_MODEL:
|
||||
|
||||
@@ -32,6 +32,7 @@ dependencies = [
|
||||
"arxiv>=2.2.0",
|
||||
"mcp>=1.6.0",
|
||||
"langchain-mcp-adapters>=0.0.9",
|
||||
"langchain-deepseek>=0.1.3",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -7,7 +7,8 @@ Server script for running the DeerFlow API.
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
import signal
|
||||
import sys
|
||||
import uvicorn
|
||||
|
||||
# Configure logging
|
||||
@@ -18,6 +19,17 @@ logging.basicConfig(
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def handle_shutdown(signum, frame):
|
||||
"""Handle graceful shutdown on SIGTERM/SIGINT"""
|
||||
logger.info("Received shutdown signal. Starting graceful shutdown...")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
# Register signal handlers
|
||||
signal.signal(signal.SIGTERM, handle_shutdown)
|
||||
signal.signal(signal.SIGINT, handle_shutdown)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Parse command line arguments
|
||||
parser = argparse.ArgumentParser(description="Run the DeerFlow API server")
|
||||
@@ -50,16 +62,18 @@ if __name__ == "__main__":
|
||||
|
||||
# Determine reload setting
|
||||
reload = False
|
||||
|
||||
# Command line arguments override defaults
|
||||
if args.reload:
|
||||
reload = True
|
||||
|
||||
logger.info("Starting DeerFlow API server")
|
||||
uvicorn.run(
|
||||
"src.server:app",
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
reload=reload,
|
||||
log_level=args.log_level,
|
||||
)
|
||||
try:
|
||||
logger.info(f"Starting DeerFlow API server on {args.host}:{args.port}")
|
||||
uvicorn.run(
|
||||
"src.server:app",
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
reload=reload,
|
||||
log_level=args.log_level,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start server: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -16,4 +16,5 @@ AGENT_LLM_MAP: dict[str, LLMType] = {
|
||||
"podcast_script_writer": "basic",
|
||||
"ppt_composer": "basic",
|
||||
"prose_writer": "basic",
|
||||
"prompt_enhancer": "basic",
|
||||
}
|
||||
|
||||
@@ -2,20 +2,28 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, fields
|
||||
from dataclasses import dataclass, field, fields
|
||||
from typing import Any, Optional
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from src.rag.retriever import Resource
|
||||
from src.config.report_style import ReportStyle
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Configuration:
|
||||
"""The configurable fields."""
|
||||
|
||||
resources: list[Resource] = field(
|
||||
default_factory=list
|
||||
) # Resources to be used for the research
|
||||
max_plan_iterations: int = 1 # Maximum number of plan iterations
|
||||
max_step_num: int = 3 # Maximum number of steps in a plan
|
||||
max_search_results: int = 3 # Maximum number of search results
|
||||
mcp_settings: dict = None # MCP settings, including dynamic loaded tools
|
||||
report_style: str = ReportStyle.ACADEMIC.value # Report style
|
||||
enable_deep_thinking: bool = False # Whether to enable deep thinking
|
||||
|
||||
@classmethod
|
||||
def from_runnable_config(
|
||||
|
||||
@@ -12,12 +12,14 @@ def replace_env_vars(value: str) -> str:
|
||||
return value
|
||||
if value.startswith("$"):
|
||||
env_var = value[1:]
|
||||
return os.getenv(env_var, value)
|
||||
return os.getenv(env_var, env_var)
|
||||
return value
|
||||
|
||||
|
||||
def process_dict(config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Recursively process dictionary to replace environment variables."""
|
||||
if not config:
|
||||
return {}
|
||||
result = {}
|
||||
for key, value in config.items():
|
||||
if isinstance(value, dict):
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import enum
|
||||
|
||||
|
||||
class ReportStyle(enum.Enum):
|
||||
ACADEMIC = "academic"
|
||||
POPULAR_SCIENCE = "popular_science"
|
||||
NEWS = "news"
|
||||
SOCIAL_MEDIA = "social_media"
|
||||
@@ -17,3 +17,10 @@ class SearchEngine(enum.Enum):
|
||||
|
||||
# Tool configuration
|
||||
SELECTED_SEARCH_ENGINE = os.getenv("SEARCH_API", SearchEngine.TAVILY.value)
|
||||
|
||||
|
||||
class RAGProvider(enum.Enum):
|
||||
RAGFLOW = "ragflow"
|
||||
|
||||
|
||||
SELECTED_RAG_PROVIDER = os.getenv("RAG_PROVIDER")
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
from .article import Article
|
||||
from .crawler import Crawler
|
||||
from .jina_client import JinaClient
|
||||
from .readability_extractor import ReadabilityExtractor
|
||||
|
||||
__all__ = [
|
||||
"Article",
|
||||
"Crawler",
|
||||
]
|
||||
__all__ = ["Article", "Crawler", "JinaClient", "ReadabilityExtractor"]
|
||||
|
||||
@@ -26,13 +26,3 @@ class Crawler:
|
||||
article = extractor.extract_article(html)
|
||||
article.url = url
|
||||
return article
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) == 2:
|
||||
url = sys.argv[1]
|
||||
else:
|
||||
url = "https://fintel.io/zh-hant/s/br/nvdc34"
|
||||
crawler = Crawler()
|
||||
article = crawler.crawl(url)
|
||||
print(article.to_markdown())
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from src.prompts.planner_model import StepType
|
||||
|
||||
from .types import State
|
||||
from .nodes import (
|
||||
@@ -17,6 +18,22 @@ from .nodes import (
|
||||
)
|
||||
|
||||
|
||||
def continue_to_running_research_team(state: State):
|
||||
current_plan = state.get("current_plan")
|
||||
if not current_plan or not current_plan.steps:
|
||||
return "planner"
|
||||
if all(step.execution_res for step in current_plan.steps):
|
||||
return "planner"
|
||||
for step in current_plan.steps:
|
||||
if not step.execution_res:
|
||||
break
|
||||
if step.step_type and step.step_type == StepType.RESEARCH:
|
||||
return "researcher"
|
||||
if step.step_type and step.step_type == StepType.PROCESSING:
|
||||
return "coder"
|
||||
return "planner"
|
||||
|
||||
|
||||
def _build_base_graph():
|
||||
"""Build and return the base state graph with all nodes and edges."""
|
||||
builder = StateGraph(State)
|
||||
@@ -29,6 +46,12 @@ def _build_base_graph():
|
||||
builder.add_node("researcher", researcher_node)
|
||||
builder.add_node("coder", coder_node)
|
||||
builder.add_node("human_feedback", human_feedback_node)
|
||||
builder.add_edge("background_investigator", "planner")
|
||||
builder.add_conditional_edges(
|
||||
"research_team",
|
||||
continue_to_running_research_team,
|
||||
["planner", "researcher", "coder"],
|
||||
)
|
||||
builder.add_edge("reporter", END)
|
||||
return builder
|
||||
|
||||
|
||||
+66
-51
@@ -17,13 +17,14 @@ from src.tools.search import LoggedTavilySearch
|
||||
from src.tools import (
|
||||
crawl_tool,
|
||||
get_web_search_tool,
|
||||
get_retriever_tool,
|
||||
python_repl_tool,
|
||||
)
|
||||
|
||||
from src.config.agents import AGENT_LLM_MAP
|
||||
from src.config.configuration import Configuration
|
||||
from src.llms.llm import get_llm_by_type
|
||||
from src.prompts.planner_model import Plan, StepType
|
||||
from src.prompts.planner_model import Plan
|
||||
from src.prompts.template import apply_prompt_template
|
||||
from src.utils.json_utils import repair_json_output
|
||||
|
||||
@@ -35,7 +36,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@tool
|
||||
def handoff_to_planner(
|
||||
task_title: Annotated[str, "The title of the task to be handed off."],
|
||||
research_topic: Annotated[str, "The topic of the research task to be handed off."],
|
||||
locale: Annotated[str, "The user's detected language locale (e.g., en-US, zh-CN)."],
|
||||
):
|
||||
"""Handoff to planner agent to do plan."""
|
||||
@@ -44,22 +45,24 @@ def handoff_to_planner(
|
||||
return
|
||||
|
||||
|
||||
def background_investigation_node(
|
||||
state: State, config: RunnableConfig
|
||||
) -> Command[Literal["planner"]]:
|
||||
def background_investigation_node(state: State, config: RunnableConfig):
|
||||
logger.info("background investigation node is running.")
|
||||
configurable = Configuration.from_runnable_config(config)
|
||||
query = state["messages"][-1].content
|
||||
if SELECTED_SEARCH_ENGINE == SearchEngine.TAVILY:
|
||||
query = state.get("research_topic")
|
||||
background_investigation_results = None
|
||||
if SELECTED_SEARCH_ENGINE == SearchEngine.TAVILY.value:
|
||||
searched_content = LoggedTavilySearch(
|
||||
max_results=configurable.max_search_results
|
||||
).invoke({"query": query})
|
||||
background_investigation_results = None
|
||||
).invoke(query)
|
||||
if isinstance(searched_content, list):
|
||||
background_investigation_results = [
|
||||
{"title": elem["title"], "content": elem["content"]}
|
||||
for elem in searched_content
|
||||
f"## {elem['title']}\n\n{elem['content']}" for elem in searched_content
|
||||
]
|
||||
return {
|
||||
"background_investigation_results": "\n\n".join(
|
||||
background_investigation_results
|
||||
)
|
||||
}
|
||||
else:
|
||||
logger.error(
|
||||
f"Tavily search returned malformed response: {searched_content}"
|
||||
@@ -68,14 +71,11 @@ def background_investigation_node(
|
||||
background_investigation_results = get_web_search_tool(
|
||||
configurable.max_search_results
|
||||
).invoke(query)
|
||||
return Command(
|
||||
update={
|
||||
"background_investigation_results": json.dumps(
|
||||
background_investigation_results, ensure_ascii=False
|
||||
)
|
||||
},
|
||||
goto="planner",
|
||||
)
|
||||
return {
|
||||
"background_investigation_results": json.dumps(
|
||||
background_investigation_results, ensure_ascii=False
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def planner_node(
|
||||
@@ -87,10 +87,8 @@ def planner_node(
|
||||
plan_iterations = state["plan_iterations"] if state.get("plan_iterations", 0) else 0
|
||||
messages = apply_prompt_template("planner", state, configurable)
|
||||
|
||||
if (
|
||||
plan_iterations == 0
|
||||
and state.get("enable_background_investigation")
|
||||
and state.get("background_investigation_results")
|
||||
if state.get("enable_background_investigation") and state.get(
|
||||
"background_investigation_results"
|
||||
):
|
||||
messages += [
|
||||
{
|
||||
@@ -103,8 +101,10 @@ def planner_node(
|
||||
}
|
||||
]
|
||||
|
||||
if AGENT_LLM_MAP["planner"] == "basic":
|
||||
llm = get_llm_by_type(AGENT_LLM_MAP["planner"]).with_structured_output(
|
||||
if configurable.enable_deep_thinking:
|
||||
llm = get_llm_by_type("reasoning")
|
||||
elif AGENT_LLM_MAP["planner"] == "basic":
|
||||
llm = get_llm_by_type("basic").with_structured_output(
|
||||
Plan,
|
||||
method="json_mode",
|
||||
)
|
||||
@@ -116,7 +116,7 @@ def planner_node(
|
||||
return Command(goto="reporter")
|
||||
|
||||
full_response = ""
|
||||
if AGENT_LLM_MAP["planner"] == "basic":
|
||||
if AGENT_LLM_MAP["planner"] == "basic" and not configurable.enable_deep_thinking:
|
||||
response = llm.invoke(messages)
|
||||
full_response = response.model_dump_json(indent=4, exclude_none=True)
|
||||
else:
|
||||
@@ -206,10 +206,11 @@ def human_feedback_node(
|
||||
|
||||
|
||||
def coordinator_node(
|
||||
state: State,
|
||||
state: State, config: RunnableConfig
|
||||
) -> Command[Literal["planner", "background_investigator", "__end__"]]:
|
||||
"""Coordinator node that communicate with customers."""
|
||||
logger.info("Coordinator talking.")
|
||||
configurable = Configuration.from_runnable_config(config)
|
||||
messages = apply_prompt_template("coordinator", state)
|
||||
response = (
|
||||
get_llm_by_type(AGENT_LLM_MAP["coordinator"])
|
||||
@@ -220,6 +221,7 @@ def coordinator_node(
|
||||
|
||||
goto = "__end__"
|
||||
locale = state.get("locale", "en-US") # Default locale if not specified
|
||||
research_topic = state.get("research_topic", "")
|
||||
|
||||
if len(response.tool_calls) > 0:
|
||||
goto = "planner"
|
||||
@@ -230,8 +232,11 @@ def coordinator_node(
|
||||
for tool_call in response.tool_calls:
|
||||
if tool_call.get("name", "") != "handoff_to_planner":
|
||||
continue
|
||||
if tool_locale := tool_call.get("args", {}).get("locale"):
|
||||
locale = tool_locale
|
||||
if tool_call.get("args", {}).get("locale") and tool_call.get(
|
||||
"args", {}
|
||||
).get("research_topic"):
|
||||
locale = tool_call.get("args", {}).get("locale")
|
||||
research_topic = tool_call.get("args", {}).get("research_topic")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing tool calls: {e}")
|
||||
@@ -242,14 +247,19 @@ def coordinator_node(
|
||||
logger.debug(f"Coordinator response: {response}")
|
||||
|
||||
return Command(
|
||||
update={"locale": locale},
|
||||
update={
|
||||
"locale": locale,
|
||||
"research_topic": research_topic,
|
||||
"resources": configurable.resources,
|
||||
},
|
||||
goto=goto,
|
||||
)
|
||||
|
||||
|
||||
def reporter_node(state: State):
|
||||
def reporter_node(state: State, config: RunnableConfig):
|
||||
"""Reporter node that write a final report."""
|
||||
logger.info("Reporter write final report")
|
||||
configurable = Configuration.from_runnable_config(config)
|
||||
current_plan = state.get("current_plan")
|
||||
input_ = {
|
||||
"messages": [
|
||||
@@ -259,7 +269,7 @@ def reporter_node(state: State):
|
||||
],
|
||||
"locale": state.get("locale", "en-US"),
|
||||
}
|
||||
invoke_messages = apply_prompt_template("reporter", input_)
|
||||
invoke_messages = apply_prompt_template("reporter", input_, configurable)
|
||||
observations = state.get("observations", [])
|
||||
|
||||
# Add a reminder about the new report format, citation style, and table usage
|
||||
@@ -285,24 +295,10 @@ def reporter_node(state: State):
|
||||
return {"final_report": response_content}
|
||||
|
||||
|
||||
def research_team_node(
|
||||
state: State,
|
||||
) -> Command[Literal["planner", "researcher", "coder"]]:
|
||||
def research_team_node(state: State):
|
||||
"""Research team node that collaborates on tasks."""
|
||||
logger.info("Research team is collaborating on tasks.")
|
||||
current_plan = state.get("current_plan")
|
||||
if not current_plan or not current_plan.steps:
|
||||
return Command(goto="planner")
|
||||
if all(step.execution_res for step in current_plan.steps):
|
||||
return Command(goto="planner")
|
||||
for step in current_plan.steps:
|
||||
if not step.execution_res:
|
||||
break
|
||||
if step.step_type and step.step_type == StepType.RESEARCH:
|
||||
return Command(goto="researcher")
|
||||
if step.step_type and step.step_type == StepType.PROCESSING:
|
||||
return Command(goto="coder")
|
||||
return Command(goto="planner")
|
||||
pass
|
||||
|
||||
|
||||
async def _execute_agent_step(
|
||||
@@ -326,14 +322,14 @@ async def _execute_agent_step(
|
||||
logger.warning("No unexecuted step found")
|
||||
return Command(goto="research_team")
|
||||
|
||||
logger.info(f"Executing step: {current_step.title}")
|
||||
logger.info(f"Executing step: {current_step.title}, agent: {agent_name}")
|
||||
|
||||
# Format completed steps information
|
||||
completed_steps_info = ""
|
||||
if completed_steps:
|
||||
completed_steps_info = "# Existing Research Findings\n\n"
|
||||
for i, step in enumerate(completed_steps):
|
||||
completed_steps_info += f"## Existing Finding {i+1}: {step.title}\n\n"
|
||||
completed_steps_info += f"## Existing Finding {i + 1}: {step.title}\n\n"
|
||||
completed_steps_info += f"<finding>\n{step.execution_res}\n</finding>\n\n"
|
||||
|
||||
# Prepare the input for the agent with completed steps info
|
||||
@@ -347,6 +343,19 @@ async def _execute_agent_step(
|
||||
|
||||
# Add citation reminder for researcher agent
|
||||
if agent_name == "researcher":
|
||||
if state.get("resources"):
|
||||
resources_info = "**The user mentioned the following resource files:**\n\n"
|
||||
for resource in state.get("resources"):
|
||||
resources_info += f"- {resource.title} ({resource.description})\n"
|
||||
|
||||
agent_input["messages"].append(
|
||||
HumanMessage(
|
||||
content=resources_info
|
||||
+ "\n\n"
|
||||
+ "You MUST use the **local_search_tool** to retrieve the information from the resource files.",
|
||||
)
|
||||
)
|
||||
|
||||
agent_input["messages"].append(
|
||||
HumanMessage(
|
||||
content="IMPORTANT: DO NOT include inline citations in the text. Instead, track all sources and include a References section at the end using link reference format. Include an empty line between each citation for better readability. Use this format for each reference:\n- [Source Title](URL)\n\n- [Another Source](URL)",
|
||||
@@ -377,6 +386,7 @@ async def _execute_agent_step(
|
||||
)
|
||||
recursion_limit = default_recursion_limit
|
||||
|
||||
logger.info(f"Agent input: {agent_input}")
|
||||
result = await agent.ainvoke(
|
||||
input=agent_input, config={"recursion_limit": recursion_limit}
|
||||
)
|
||||
@@ -468,11 +478,16 @@ async def researcher_node(
|
||||
"""Researcher node that do research"""
|
||||
logger.info("Researcher node is researching.")
|
||||
configurable = Configuration.from_runnable_config(config)
|
||||
tools = [get_web_search_tool(configurable.max_search_results), crawl_tool]
|
||||
retriever_tool = get_retriever_tool(state.get("resources", []))
|
||||
if retriever_tool:
|
||||
tools.insert(0, retriever_tool)
|
||||
logger.info(f"Researcher tools: {tools}")
|
||||
return await _setup_and_execute_agent_step(
|
||||
state,
|
||||
config,
|
||||
"researcher",
|
||||
[get_web_search_tool(configurable.max_search_results), crawl_tool],
|
||||
tools,
|
||||
)
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -1,12 +1,10 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import operator
|
||||
from typing import Annotated
|
||||
|
||||
from langgraph.graph import MessagesState
|
||||
|
||||
from src.prompts.planner_model import Plan
|
||||
from src.rag import Resource
|
||||
|
||||
|
||||
class State(MessagesState):
|
||||
@@ -14,7 +12,9 @@ class State(MessagesState):
|
||||
|
||||
# Runtime Variables
|
||||
locale: str = "en-US"
|
||||
research_topic: str = ""
|
||||
observations: list[str] = []
|
||||
resources: list[Resource] = []
|
||||
plan_iterations: int = 0
|
||||
current_plan: Plan | str = None
|
||||
final_report: str = ""
|
||||
|
||||
+99
-14
@@ -3,8 +3,11 @@
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
import os
|
||||
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain_deepseek import ChatDeepSeek
|
||||
from typing import get_args
|
||||
|
||||
from src.config import load_yaml_config
|
||||
from src.config.agents import LLMType
|
||||
@@ -13,18 +16,66 @@ from src.config.agents import LLMType
|
||||
_llm_cache: dict[LLMType, ChatOpenAI] = {}
|
||||
|
||||
|
||||
def _create_llm_use_conf(llm_type: LLMType, conf: Dict[str, Any]) -> ChatOpenAI:
|
||||
llm_type_map = {
|
||||
"reasoning": conf.get("REASONING_MODEL"),
|
||||
"basic": conf.get("BASIC_MODEL"),
|
||||
"vision": conf.get("VISION_MODEL"),
|
||||
def _get_config_file_path() -> str:
|
||||
"""Get the path to the configuration file."""
|
||||
return str((Path(__file__).parent.parent.parent / "conf.yaml").resolve())
|
||||
|
||||
|
||||
def _get_llm_type_config_keys() -> dict[str, str]:
|
||||
"""Get mapping of LLM types to their configuration keys."""
|
||||
return {
|
||||
"reasoning": "REASONING_MODEL",
|
||||
"basic": "BASIC_MODEL",
|
||||
"vision": "VISION_MODEL",
|
||||
}
|
||||
llm_conf = llm_type_map.get(llm_type)
|
||||
if not llm_conf:
|
||||
|
||||
|
||||
def _get_env_llm_conf(llm_type: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get LLM configuration from environment variables.
|
||||
Environment variables should follow the format: {LLM_TYPE}__{KEY}
|
||||
e.g., BASIC_MODEL__api_key, BASIC_MODEL__base_url
|
||||
"""
|
||||
prefix = f"{llm_type.upper()}_MODEL__"
|
||||
conf = {}
|
||||
for key, value in os.environ.items():
|
||||
if key.startswith(prefix):
|
||||
conf_key = key[len(prefix) :].lower()
|
||||
conf[conf_key] = value
|
||||
return conf
|
||||
|
||||
|
||||
def _create_llm_use_conf(
|
||||
llm_type: LLMType, conf: Dict[str, Any]
|
||||
) -> ChatOpenAI | ChatDeepSeek:
|
||||
"""Create LLM instance using configuration."""
|
||||
llm_type_config_keys = _get_llm_type_config_keys()
|
||||
config_key = llm_type_config_keys.get(llm_type)
|
||||
|
||||
if not config_key:
|
||||
raise ValueError(f"Unknown LLM type: {llm_type}")
|
||||
|
||||
llm_conf = conf.get(config_key, {})
|
||||
if not isinstance(llm_conf, dict):
|
||||
raise ValueError(f"Invalid LLM Conf: {llm_type}")
|
||||
return ChatOpenAI(**llm_conf)
|
||||
raise ValueError(f"Invalid LLM configuration for {llm_type}: {llm_conf}")
|
||||
|
||||
# Get configuration from environment variables
|
||||
env_conf = _get_env_llm_conf(llm_type)
|
||||
|
||||
# Merge configurations, with environment variables taking precedence
|
||||
merged_conf = {**llm_conf, **env_conf}
|
||||
|
||||
if not merged_conf:
|
||||
raise ValueError(f"No configuration found for LLM type: {llm_type}")
|
||||
|
||||
if llm_type == "reasoning":
|
||||
merged_conf["api_base"] = merged_conf.pop("base_url", None)
|
||||
|
||||
return (
|
||||
ChatOpenAI(**merged_conf)
|
||||
if llm_type != "reasoning"
|
||||
else ChatDeepSeek(**merged_conf)
|
||||
)
|
||||
|
||||
|
||||
def get_llm_by_type(
|
||||
@@ -36,16 +87,48 @@ def get_llm_by_type(
|
||||
if llm_type in _llm_cache:
|
||||
return _llm_cache[llm_type]
|
||||
|
||||
conf = load_yaml_config(
|
||||
str((Path(__file__).parent.parent.parent / "conf.yaml").resolve())
|
||||
)
|
||||
conf = load_yaml_config(_get_config_file_path())
|
||||
llm = _create_llm_use_conf(llm_type, conf)
|
||||
_llm_cache[llm_type] = llm
|
||||
return llm
|
||||
|
||||
|
||||
# Initialize LLMs for different purposes - now these will be cached
|
||||
basic_llm = get_llm_by_type("basic")
|
||||
def get_configured_llm_models() -> dict[str, list[str]]:
|
||||
"""
|
||||
Get all configured LLM models grouped by type.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping LLM type to list of configured model names.
|
||||
"""
|
||||
try:
|
||||
conf = load_yaml_config(_get_config_file_path())
|
||||
llm_type_config_keys = _get_llm_type_config_keys()
|
||||
|
||||
configured_models: dict[str, list[str]] = {}
|
||||
|
||||
for llm_type in get_args(LLMType):
|
||||
# Get configuration from YAML file
|
||||
config_key = llm_type_config_keys.get(llm_type, "")
|
||||
yaml_conf = conf.get(config_key, {}) if config_key else {}
|
||||
|
||||
# Get configuration from environment variables
|
||||
env_conf = _get_env_llm_conf(llm_type)
|
||||
|
||||
# Merge configurations, with environment variables taking precedence
|
||||
merged_conf = {**yaml_conf, **env_conf}
|
||||
|
||||
# Check if model is configured
|
||||
model_name = merged_conf.get("model")
|
||||
if model_name:
|
||||
configured_models.setdefault(llm_type, []).append(model_name)
|
||||
|
||||
return configured_models
|
||||
|
||||
except Exception as e:
|
||||
# Log error and return empty dict to avoid breaking the application
|
||||
print(f"Warning: Failed to load LLM configuration: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
# In the future, we will use reasoning_llm and vl_llm for different purposes
|
||||
# reasoning_llm = get_llm_by_type("reasoning")
|
||||
@@ -53,4 +136,6 @@ basic_llm = get_llm_by_type("basic")
|
||||
|
||||
|
||||
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"))
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""Prompt enhancer module for improving user prompts."""
|
||||
@@ -0,0 +1,25 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from langgraph.graph import StateGraph
|
||||
|
||||
from src.prompt_enhancer.graph.enhancer_node import prompt_enhancer_node
|
||||
from src.prompt_enhancer.graph.state import PromptEnhancerState
|
||||
|
||||
|
||||
def build_graph():
|
||||
"""Build and return the prompt enhancer workflow graph."""
|
||||
# Build state graph
|
||||
builder = StateGraph(PromptEnhancerState)
|
||||
|
||||
# Add the enhancer node
|
||||
builder.add_node("enhancer", prompt_enhancer_node)
|
||||
|
||||
# Set entry point
|
||||
builder.set_entry_point("enhancer")
|
||||
|
||||
# Set finish point
|
||||
builder.set_finish_point("enhancer")
|
||||
|
||||
# Compile and return the graph
|
||||
return builder.compile()
|
||||
@@ -0,0 +1,67 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import logging
|
||||
|
||||
from langchain.schema import HumanMessage, SystemMessage
|
||||
|
||||
from src.config.agents import AGENT_LLM_MAP
|
||||
from src.llms.llm import get_llm_by_type
|
||||
from src.prompts.template import env, apply_prompt_template
|
||||
from src.prompt_enhancer.graph.state import PromptEnhancerState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def prompt_enhancer_node(state: PromptEnhancerState):
|
||||
"""Node that enhances user prompts using AI analysis."""
|
||||
logger.info("Enhancing user prompt...")
|
||||
|
||||
model = get_llm_by_type(AGENT_LLM_MAP["prompt_enhancer"])
|
||||
|
||||
try:
|
||||
|
||||
# Create messages with context if provided
|
||||
context_info = ""
|
||||
if state.get("context"):
|
||||
context_info = f"\n\nAdditional context: {state['context']}"
|
||||
|
||||
original_prompt_message = HumanMessage(
|
||||
content=f"Please enhance this prompt:{context_info}\n\nOriginal prompt: {state['prompt']}"
|
||||
)
|
||||
|
||||
messages = apply_prompt_template(
|
||||
"prompt_enhancer/prompt_enhancer",
|
||||
{
|
||||
"messages": [original_prompt_message],
|
||||
"report_style": state.get("report_style"),
|
||||
},
|
||||
)
|
||||
|
||||
# Get the response from the model
|
||||
response = model.invoke(messages)
|
||||
|
||||
# Clean up the response - remove any extra formatting or comments
|
||||
enhanced_prompt = response.content.strip()
|
||||
|
||||
# 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.debug(f"Enhanced prompt: {enhanced_prompt}")
|
||||
return {"output": enhanced_prompt}
|
||||
except Exception as e:
|
||||
logger.error(f"Error in prompt enhancement: {str(e)}")
|
||||
return {"output": state["prompt"]}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from typing import TypedDict, Optional
|
||||
from src.config.report_style import ReportStyle
|
||||
|
||||
|
||||
class PromptEnhancerState(TypedDict):
|
||||
"""State for the prompt enhancer workflow."""
|
||||
|
||||
prompt: str # Original prompt to enhance
|
||||
context: Optional[str] # Additional context
|
||||
report_style: Optional[ReportStyle] # Report style preference
|
||||
output: Optional[str] # Enhanced prompt result
|
||||
+24
-23
@@ -57,14 +57,15 @@ Before creating a detailed plan, assess if there is sufficient context to answer
|
||||
|
||||
Different types of steps have different web search requirements:
|
||||
|
||||
1. **Research Steps** (`need_web_search: true`):
|
||||
1. **Research Steps** (`need_search: true`):
|
||||
- Retrieve information from the file with the URL with `rag://` or `http://` prefix specified by the user
|
||||
- Gathering market data or industry trends
|
||||
- Finding historical information
|
||||
- Collecting competitor analysis
|
||||
- Researching current events or news
|
||||
- Finding statistical data or reports
|
||||
|
||||
2. **Data Processing Steps** (`need_web_search: false`):
|
||||
2. **Data Processing Steps** (`need_search: false`):
|
||||
- API calls and data extraction
|
||||
- Database queries
|
||||
- Raw data collection from existing sources
|
||||
@@ -74,10 +75,10 @@ Different types of steps have different web search requirements:
|
||||
## Exclusions
|
||||
|
||||
- **No Direct Calculations in Research Steps**:
|
||||
- Research steps should only gather data and information
|
||||
- All mathematical calculations must be handled by processing steps
|
||||
- Numerical analysis must be delegated to processing steps
|
||||
- Research steps focus on information gathering only
|
||||
- Research steps should only gather data and information
|
||||
- All mathematical calculations must be handled by processing steps
|
||||
- Numerical analysis must be delegated to processing steps
|
||||
- Research steps focus on information gathering only
|
||||
|
||||
## Analysis Framework
|
||||
|
||||
@@ -135,16 +136,16 @@ When planning information gathering, consider these key aspects and ensure COMPR
|
||||
- To begin with, repeat user's requirement in your own words as `thought`.
|
||||
- Rigorously assess if there is sufficient context to answer the question using the strict criteria above.
|
||||
- If context is sufficient:
|
||||
- Set `has_enough_context` to true
|
||||
- No need to create information gathering steps
|
||||
- Set `has_enough_context` to true
|
||||
- No need to create information gathering steps
|
||||
- If context is insufficient (default assumption):
|
||||
- Break down the required information using the Analysis Framework
|
||||
- Create NO MORE THAN {{ max_step_num }} focused and comprehensive steps that cover the most essential aspects
|
||||
- Ensure each step is substantial and covers related information categories
|
||||
- Prioritize breadth and depth within the {{ max_step_num }}-step constraint
|
||||
- For each step, carefully assess if web search is needed:
|
||||
- Research and external data gathering: Set `need_web_search: true`
|
||||
- Internal data processing: Set `need_web_search: false`
|
||||
- Break down the required information using the Analysis Framework
|
||||
- Create NO MORE THAN {{ max_step_num }} focused and comprehensive steps that cover the most essential aspects
|
||||
- Ensure each step is substantial and covers related information categories
|
||||
- Prioritize breadth and depth within the {{ max_step_num }}-step constraint
|
||||
- For each step, carefully assess if web search is needed:
|
||||
- Research and external data gathering: Set `need_search: true`
|
||||
- Internal data processing: Set `need_search: false`
|
||||
- Specify the exact data to be collected in step's `description`. Include a `note` if necessary.
|
||||
- Prioritize depth and volume of relevant information - limited information is not acceptable.
|
||||
- Use the same language as the user to generate the plan.
|
||||
@@ -156,10 +157,10 @@ Directly output the raw JSON format of `Plan` without "```json". The `Plan` inte
|
||||
|
||||
```ts
|
||||
interface Step {
|
||||
need_web_search: boolean; // Must be explicitly set for each step
|
||||
need_search: boolean; // Must be explicitly set for each step
|
||||
title: string;
|
||||
description: string; // Specify exactly what data to collect
|
||||
step_type: "research" | "processing"; // Indicates the nature of the step
|
||||
description: string; // Specify exactly what data to collect. If the user input contains a link, please retain the full Markdown format when necessary.
|
||||
step_type: "research" | "processing"; // Indicates the nature of the step
|
||||
}
|
||||
|
||||
interface Plan {
|
||||
@@ -167,7 +168,7 @@ interface Plan {
|
||||
has_enough_context: boolean;
|
||||
thought: string;
|
||||
title: string;
|
||||
steps: Step[]; // Research & Processing steps to get more context
|
||||
steps: Step[]; // Research & Processing steps to get more context
|
||||
}
|
||||
```
|
||||
|
||||
@@ -179,8 +180,8 @@ interface Plan {
|
||||
- Prioritize BOTH breadth (covering essential aspects) AND depth (detailed information on each aspect)
|
||||
- Never settle for minimal information - the goal is a comprehensive, detailed final report
|
||||
- Limited or insufficient information will lead to an inadequate final report
|
||||
- Carefully assess each step's web search requirement based on its nature:
|
||||
- Research steps (`need_web_search: true`) for gathering information
|
||||
- Processing steps (`need_web_search: false`) for calculations and data processing
|
||||
- Carefully assess each step's web search or retrieve from URL requirement based on its nature:
|
||||
- Research steps (`need_search: true`) for gathering information
|
||||
- Processing steps (`need_search: false`) for calculations and data processing
|
||||
- Default to gathering more information unless the strictest sufficient context criteria are met
|
||||
- Always use the language specified by the locale = **{{ locale }}**.
|
||||
- Always use the language specified by the locale = **{{ locale }}**.
|
||||
|
||||
@@ -13,9 +13,7 @@ class StepType(str, Enum):
|
||||
|
||||
|
||||
class Step(BaseModel):
|
||||
need_web_search: bool = Field(
|
||||
..., description="Must be explicitly set for each step"
|
||||
)
|
||||
need_search: bool = Field(..., description="Must be explicitly set for each step")
|
||||
title: str
|
||||
description: str = Field(..., description="Specify exactly what data to collect")
|
||||
step_type: StepType = Field(..., description="Indicates the nature of the step")
|
||||
@@ -47,7 +45,7 @@ class Plan(BaseModel):
|
||||
"title": "AI Market Research Plan",
|
||||
"steps": [
|
||||
{
|
||||
"need_web_search": True,
|
||||
"need_search": True,
|
||||
"title": "Current AI Market Analysis",
|
||||
"description": (
|
||||
"Collect data on market size, growth rates, major players, and investment trends in AI sector."
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
CURRENT_TIME: {{ CURRENT_TIME }}
|
||||
---
|
||||
|
||||
You are an expert prompt engineer. Your task is to enhance user prompts to make them more effective, specific, and likely to produce high-quality results from AI systems.
|
||||
|
||||
# Your Role
|
||||
- Analyze the original prompt for clarity, specificity, and completeness
|
||||
- Enhance the prompt by adding relevant details, context, and structure
|
||||
- Make the prompt more actionable and results-oriented
|
||||
- Preserve the user's original intent while improving effectiveness
|
||||
|
||||
{% if report_style == "academic" %}
|
||||
# Enhancement Guidelines for Academic Style
|
||||
1. **Add methodological rigor**: Include research methodology, scope, and analytical framework
|
||||
2. **Specify academic structure**: Organize with clear thesis, literature review, analysis, and conclusions
|
||||
3. **Clarify scholarly expectations**: Specify citation requirements, evidence standards, and academic tone
|
||||
4. **Add theoretical context**: Include relevant theoretical frameworks and disciplinary perspectives
|
||||
5. **Ensure precision**: Use precise terminology and avoid ambiguous language
|
||||
6. **Include limitations**: Acknowledge scope limitations and potential biases
|
||||
{% elif report_style == "popular_science" %}
|
||||
# Enhancement Guidelines for Popular Science Style
|
||||
1. **Add accessibility**: Transform technical concepts into relatable analogies and examples
|
||||
2. **Improve narrative structure**: Organize as an engaging story with clear beginning, middle, and end
|
||||
3. **Clarify audience expectations**: Specify general audience level and engagement goals
|
||||
4. **Add human context**: Include real-world applications and human interest elements
|
||||
5. **Make it compelling**: Ensure the prompt guides toward fascinating and wonder-inspiring content
|
||||
6. **Include visual elements**: Suggest use of metaphors and descriptive language for complex concepts
|
||||
{% elif report_style == "news" %}
|
||||
# Enhancement Guidelines for News Style
|
||||
1. **Add journalistic rigor**: Include fact-checking requirements, source verification, and objectivity standards
|
||||
2. **Improve news structure**: Organize with inverted pyramid structure (most important information first)
|
||||
3. **Clarify reporting expectations**: Specify timeliness, accuracy, and balanced perspective requirements
|
||||
4. **Add contextual background**: Include relevant background information and broader implications
|
||||
5. **Make it newsworthy**: Ensure the prompt focuses on current relevance and public interest
|
||||
6. **Include attribution**: Specify source requirements and quote standards
|
||||
{% elif report_style == "social_media" %}
|
||||
# Enhancement Guidelines for Social Media Style
|
||||
1. **Add engagement focus**: Include attention-grabbing elements, hooks, and shareability factors
|
||||
2. **Improve platform structure**: Organize for specific platform requirements (character limits, hashtags, etc.)
|
||||
3. **Clarify audience expectations**: Specify target demographic and engagement goals
|
||||
4. **Add viral elements**: Include trending topics, relatable content, and interactive elements
|
||||
5. **Make it shareable**: Ensure the prompt guides toward content that encourages sharing and discussion
|
||||
6. **Include visual considerations**: Suggest emoji usage, formatting, and visual appeal elements
|
||||
{% else %}
|
||||
# General Enhancement Guidelines
|
||||
1. **Add specificity**: Include relevant details, scope, and constraints
|
||||
2. **Improve structure**: Organize the request logically with clear sections if needed
|
||||
3. **Clarify expectations**: Specify desired output format, length, or style
|
||||
4. **Add context**: Include background information that would help generate better results
|
||||
5. **Make it actionable**: Ensure the prompt guides toward concrete, useful outputs
|
||||
{% endif %}
|
||||
|
||||
# Output Requirements
|
||||
- Output ONLY the enhanced prompt
|
||||
- Do NOT include any explanations, comments, or meta-text
|
||||
- Do NOT use phrases like "Enhanced Prompt:" or "Here's the enhanced version:"
|
||||
- The output should be ready to use directly as a prompt
|
||||
|
||||
{% if report_style == "academic" %}
|
||||
# Academic Style Examples
|
||||
|
||||
**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."
|
||||
|
||||
**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."
|
||||
|
||||
{% elif report_style == "popular_science" %}
|
||||
# Popular Science Style Examples
|
||||
|
||||
**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."
|
||||
|
||||
**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."
|
||||
|
||||
{% elif report_style == "news" %}
|
||||
# News Style Examples
|
||||
|
||||
**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."
|
||||
|
||||
**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."
|
||||
|
||||
{% elif report_style == "social_media" %}
|
||||
# Social Media Style Examples
|
||||
|
||||
**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."
|
||||
|
||||
**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."
|
||||
|
||||
{% else %}
|
||||
# General Examples
|
||||
|
||||
**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."
|
||||
|
||||
**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."
|
||||
{% endif %}
|
||||
+159
-2
@@ -2,7 +2,21 @@
|
||||
CURRENT_TIME: {{ CURRENT_TIME }}
|
||||
---
|
||||
|
||||
You are a professional reporter responsible for writing clear, comprehensive reports based ONLY on provided information and verifiable facts.
|
||||
{% if report_style == "academic" %}
|
||||
You are a distinguished academic researcher and scholarly writer. Your report must embody the highest standards of academic rigor and intellectual discourse. Write with the precision of a peer-reviewed journal article, employing sophisticated analytical frameworks, comprehensive literature synthesis, and methodological transparency. Your language should be formal, technical, and authoritative, utilizing discipline-specific terminology with exactitude. Structure arguments logically with clear thesis statements, supporting evidence, and nuanced conclusions. Maintain complete objectivity, acknowledge limitations, and present balanced perspectives on controversial topics. The report should demonstrate deep scholarly engagement and contribute meaningfully to academic knowledge.
|
||||
{% elif report_style == "popular_science" %}
|
||||
You are an award-winning science communicator and storyteller. Your mission is to transform complex scientific concepts into captivating narratives that spark curiosity and wonder in everyday readers. Write with the enthusiasm of a passionate educator, using vivid analogies, relatable examples, and compelling storytelling techniques. Your tone should be warm, approachable, and infectious in its excitement about discovery. Break down technical jargon into accessible language without sacrificing accuracy. Use metaphors, real-world comparisons, and human interest angles to make abstract concepts tangible. Think like a National Geographic writer or a TED Talk presenter - engaging, enlightening, and inspiring.
|
||||
{% elif report_style == "news" %}
|
||||
You are an NBC News correspondent and investigative journalist with decades of experience in breaking news and in-depth reporting. Your report must exemplify the gold standard of American broadcast journalism: authoritative, meticulously researched, and delivered with the gravitas and credibility that NBC News is known for. Write with the precision of a network news anchor, employing the classic inverted pyramid structure while weaving compelling human narratives. Your language should be clear, authoritative, and accessible to prime-time television audiences. Maintain NBC's tradition of balanced reporting, thorough fact-checking, and ethical journalism. Think like Lester Holt or Andrea Mitchell - delivering complex stories with clarity, context, and unwavering integrity.
|
||||
{% elif report_style == "social_media" %}
|
||||
{% if locale == "zh-CN" %}
|
||||
You are a popular 小红书 (Xiaohongshu) content creator specializing in lifestyle and knowledge sharing. Your report should embody the authentic, personal, and engaging style that resonates with 小红书 users. Write with genuine enthusiasm and a "姐妹们" (sisters) tone, as if sharing exciting discoveries with close friends. Use abundant emojis, create "种草" (grass-planting/recommendation) moments, and structure content for easy mobile consumption. Your writing should feel like a personal diary entry mixed with expert insights - warm, relatable, and irresistibly shareable. Think like a top 小红书 blogger who effortlessly combines personal experience with valuable information, making readers feel like they've discovered a hidden gem.
|
||||
{% else %}
|
||||
You are a viral Twitter content creator and digital influencer specializing in breaking down complex topics into engaging, shareable threads. Your report should be optimized for maximum engagement and viral potential across social media platforms. Write with energy, authenticity, and a conversational tone that resonates with global online communities. Use strategic hashtags, create quotable moments, and structure content for easy consumption and sharing. Think like a successful Twitter thought leader who can make any topic accessible, engaging, and discussion-worthy while maintaining credibility and accuracy.
|
||||
{% endif %}
|
||||
{% else %}
|
||||
You are a professional reporter responsible for writing clear, comprehensive reports based ONLY on provided information and verifiable facts. Your report should adopt a professional tone.
|
||||
{% endif %}
|
||||
|
||||
# Role
|
||||
|
||||
@@ -43,10 +57,40 @@ Structure your report in the following format:
|
||||
- **Including images from the previous steps in the report is very helpful.**
|
||||
|
||||
5. **Survey Note** (for more comprehensive reports)
|
||||
{% if report_style == "academic" %}
|
||||
- **Literature Review & Theoretical Framework**: Comprehensive analysis of existing research and theoretical foundations
|
||||
- **Methodology & Data Analysis**: Detailed examination of research methods and analytical approaches
|
||||
- **Critical Discussion**: In-depth evaluation of findings with consideration of limitations and implications
|
||||
- **Future Research Directions**: Identification of gaps and recommendations for further investigation
|
||||
{% elif report_style == "popular_science" %}
|
||||
- **The Bigger Picture**: How this research fits into the broader scientific landscape
|
||||
- **Real-World Applications**: Practical implications and potential future developments
|
||||
- **Behind the Scenes**: Interesting details about the research process and challenges faced
|
||||
- **What's Next**: Exciting possibilities and upcoming developments in the field
|
||||
{% elif report_style == "news" %}
|
||||
- **NBC News Analysis**: In-depth examination of the story's broader implications and significance
|
||||
- **Impact Assessment**: How these developments affect different communities, industries, and stakeholders
|
||||
- **Expert Perspectives**: Insights from credible sources, analysts, and subject matter experts
|
||||
- **Timeline & Context**: Chronological background and historical context essential for understanding
|
||||
- **What's Next**: Expected developments, upcoming milestones, and stories to watch
|
||||
{% elif report_style == "social_media" %}
|
||||
{% if locale == "zh-CN" %}
|
||||
- **【种草时刻】**: 最值得关注的亮点和必须了解的核心信息
|
||||
- **【数据震撼】**: 用小红书风格展示重要统计数据和发现
|
||||
- **【姐妹们的看法】**: 社区热议话题和大家的真实反馈
|
||||
- **【行动指南】**: 实用建议和读者可以立即行动的清单
|
||||
{% else %}
|
||||
- **Thread Highlights**: Key takeaways formatted for maximum shareability
|
||||
- **Data That Matters**: Important statistics and findings presented for viral potential
|
||||
- **Community Pulse**: Trending discussions and reactions from the online community
|
||||
- **Action Steps**: Practical advice and immediate next steps for readers
|
||||
{% endif %}
|
||||
{% else %}
|
||||
- A more detailed, academic-style analysis.
|
||||
- Include comprehensive sections covering all aspects of the topic.
|
||||
- Can include comparative analysis, tables, and detailed feature breakdowns.
|
||||
- This section is optional for shorter reports.
|
||||
{% endif %}
|
||||
|
||||
6. **Key Citations**
|
||||
- List all references at the end in link reference format.
|
||||
@@ -56,7 +100,64 @@ Structure your report in the following format:
|
||||
# Writing Guidelines
|
||||
|
||||
1. Writing style:
|
||||
- Use professional tone.
|
||||
{% if report_style == "academic" %}
|
||||
**Academic Excellence Standards:**
|
||||
- Employ sophisticated, formal academic discourse with discipline-specific terminology
|
||||
- Construct complex, nuanced arguments with clear thesis statements and logical progression
|
||||
- Use third-person perspective and passive voice where appropriate for objectivity
|
||||
- Include methodological considerations and acknowledge research limitations
|
||||
- Reference theoretical frameworks and cite relevant scholarly work patterns
|
||||
- Maintain intellectual rigor with precise, unambiguous language
|
||||
- Avoid contractions, colloquialisms, and informal expressions entirely
|
||||
- Use hedging language appropriately ("suggests," "indicates," "appears to")
|
||||
{% elif report_style == "popular_science" %}
|
||||
**Science Communication Excellence:**
|
||||
- Write with infectious enthusiasm and genuine curiosity about discoveries
|
||||
- Transform technical jargon into vivid, relatable analogies and metaphors
|
||||
- Use active voice and engaging narrative techniques to tell scientific stories
|
||||
- Include "wow factor" moments and surprising revelations to maintain interest
|
||||
- Employ conversational tone while maintaining scientific accuracy
|
||||
- Use rhetorical questions to engage readers and guide their thinking
|
||||
- Include human elements: researcher personalities, discovery stories, real-world impacts
|
||||
- Balance accessibility with intellectual respect for your audience
|
||||
{% elif report_style == "news" %}
|
||||
**NBC News Editorial Standards:**
|
||||
- Open with a compelling lede that captures the essence of the story in 25-35 words
|
||||
- Use the classic inverted pyramid: most newsworthy information first, supporting details follow
|
||||
- Write in clear, conversational broadcast style that sounds natural when read aloud
|
||||
- Employ active voice and strong, precise verbs that convey action and urgency
|
||||
- Attribute every claim to specific, credible sources using NBC's attribution standards
|
||||
- Use present tense for ongoing situations, past tense for completed events
|
||||
- Maintain NBC's commitment to balanced reporting with multiple perspectives
|
||||
- Include essential context and background without overwhelming the main story
|
||||
- Verify information through at least two independent sources when possible
|
||||
- Clearly label speculation, analysis, and ongoing investigations
|
||||
- Use transitional phrases that guide readers smoothly through the narrative
|
||||
{% elif report_style == "social_media" %}
|
||||
{% if locale == "zh-CN" %}
|
||||
**小红书风格写作标准:**
|
||||
- 用"姐妹们!"、"宝子们!"等亲切称呼开头,营造闺蜜聊天氛围
|
||||
- 大量使用emoji表情符号增强表达力和视觉吸引力 ✨��
|
||||
- 采用"种草"语言:"真的绝了!"、"必须安利给大家!"、"不看后悔系列!"
|
||||
- 使用小红书特色标题格式:"【干货分享】"、"【亲测有效】"、"【避雷指南】"
|
||||
- 穿插个人感受和体验:"我当时看到这个数据真的震惊了!"
|
||||
- 用数字和符号增强视觉效果:①②③、✅❌、🔥💡⭐
|
||||
- 创造"金句"和可截图分享的内容段落
|
||||
- 结尾用互动性语言:"你们觉得呢?"、"评论区聊聊!"、"记得点赞收藏哦!"
|
||||
{% else %}
|
||||
**Twitter/X Engagement Standards:**
|
||||
- Open with attention-grabbing hooks that stop the scroll
|
||||
- Use thread-style formatting with numbered points (1/n, 2/n, etc.)
|
||||
- Incorporate strategic hashtags for discoverability and trending topics
|
||||
- Write quotable, tweetable snippets that beg to be shared
|
||||
- Use conversational, authentic voice with personality and wit
|
||||
- Include relevant emojis to enhance meaning and visual appeal 🧵📊💡
|
||||
- Create "thread-worthy" content with clear progression and payoff
|
||||
- End with engagement prompts: "What do you think?", "Retweet if you agree"
|
||||
{% endif %}
|
||||
{% else %}
|
||||
- Use a professional tone.
|
||||
{% endif %}
|
||||
- Be concise and precise.
|
||||
- Avoid speculation.
|
||||
- Support claims with evidence.
|
||||
@@ -77,6 +178,62 @@ Structure your report in the following format:
|
||||
- Use horizontal rules (---) to separate major sections.
|
||||
- Track the sources of information but keep the main text clean and readable.
|
||||
|
||||
{% if report_style == "academic" %}
|
||||
**Academic Formatting Specifications:**
|
||||
- Use formal section headings with clear hierarchical structure (## Introduction, ### Methodology, #### Subsection)
|
||||
- Employ numbered lists for methodological steps and logical sequences
|
||||
- Use block quotes for important definitions or key theoretical concepts
|
||||
- Include detailed tables with comprehensive headers and statistical data
|
||||
- Use footnote-style formatting for additional context or clarifications
|
||||
- Maintain consistent academic citation patterns throughout
|
||||
- Use `code blocks` for technical specifications, formulas, or data samples
|
||||
{% elif report_style == "popular_science" %}
|
||||
**Science Communication Formatting:**
|
||||
- Use engaging, descriptive headings that spark curiosity ("The Surprising Discovery That Changed Everything")
|
||||
- Employ creative formatting like callout boxes for "Did You Know?" facts
|
||||
- Use bullet points for easy-to-digest key findings
|
||||
- Include visual breaks with strategic use of bold text for emphasis
|
||||
- Format analogies and metaphors prominently to aid understanding
|
||||
- Use numbered lists for step-by-step explanations of complex processes
|
||||
- Highlight surprising statistics or findings with special formatting
|
||||
{% elif report_style == "news" %}
|
||||
**NBC News Formatting Standards:**
|
||||
- Craft headlines that are informative yet compelling, following NBC's style guide
|
||||
- Use NBC-style datelines and bylines for professional credibility
|
||||
- Structure paragraphs for broadcast readability (1-2 sentences for digital, 2-3 for print)
|
||||
- Employ strategic subheadings that advance the story narrative
|
||||
- Format direct quotes with proper attribution and context
|
||||
- Use bullet points sparingly, primarily for breaking news updates or key facts
|
||||
- Include "BREAKING" or "DEVELOPING" labels for ongoing stories
|
||||
- Format source attribution clearly: "according to NBC News," "sources tell NBC News"
|
||||
- Use italics for emphasis on key terms or breaking developments
|
||||
- Structure the story with clear sections: Lede, Context, Analysis, Looking Ahead
|
||||
{% elif report_style == "social_media" %}
|
||||
{% if locale == "zh-CN" %}
|
||||
**小红书格式优化标准:**
|
||||
- 使用吸睛标题配合emoji:"🔥【重磅】这个发现太震撼了!"
|
||||
- 关键数据用醒目格式突出:「 重点数据 」或 ⭐ 核心发现 ⭐
|
||||
- 适度使用大写强调:真的YYDS!、绝绝子!
|
||||
- 用emoji作为分点符号:✨、🌟、�、�、💯
|
||||
- 创建话题标签区域:#科技前沿 #必看干货 #涨知识了
|
||||
- 设置"划重点"总结区域,方便快速阅读
|
||||
- 利用换行和空白营造手机阅读友好的版式
|
||||
- 制作"金句卡片"格式,便于截图分享
|
||||
- 使用分割线和特殊符号:「」『』【】━━━━━━
|
||||
{% else %}
|
||||
**Twitter/X Formatting Standards:**
|
||||
- Use compelling headlines with strategic emoji placement 🧵⚡️🔥
|
||||
- Format key insights as standalone, quotable tweet blocks
|
||||
- Employ thread numbering for multi-part content (1/12, 2/12, etc.)
|
||||
- Use bullet points with emoji bullets for visual appeal
|
||||
- Include strategic hashtags at the end: #TechNews #Innovation #MustRead
|
||||
- Create "TL;DR" summaries for quick consumption
|
||||
- Use line breaks and white space for mobile readability
|
||||
- Format "quotable moments" with clear visual separation
|
||||
- Include call-to-action elements: "🔄 RT to share" "💬 What's your take?"
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
# Data Integrity
|
||||
|
||||
- Only use information explicitly provided in the input.
|
||||
|
||||
@@ -11,6 +11,9 @@ You are dedicated to conducting thorough investigations using search tools and p
|
||||
You have access to two types of tools:
|
||||
|
||||
1. **Built-in Tools**: These are always available:
|
||||
{% if resources %}
|
||||
- **local_search_tool**: For retrieving information from the local knowledge base when user mentioned in the messages.
|
||||
{% endif %}
|
||||
- **web_search_tool**: For performing web searches
|
||||
- **crawl_tool**: For reading content from URLs
|
||||
|
||||
@@ -34,7 +37,7 @@ You have access to two types of tools:
|
||||
3. **Plan the Solution**: Determine the best approach to solve the problem using the available tools.
|
||||
4. **Execute the Solution**:
|
||||
- Forget your previous knowledge, so you **should leverage the tools** to retrieve the information.
|
||||
- Use the **web_search_tool** or other suitable search tool to perform a search with the provided keywords.
|
||||
- Use the {% if resources %}**local_search_tool** or{% endif %}**web_search_tool** or other suitable search tool to perform a search with the provided keywords.
|
||||
- When the task includes time range requirements:
|
||||
- Incorporate appropriate time-based search parameters in your queries (e.g., "after:2020", "before:2023", or specific date ranges)
|
||||
- Ensure search results respect the specified time constraints.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from .retriever import Retriever, Document, Resource
|
||||
from .ragflow import RAGFlowProvider
|
||||
from .builder import build_retriever
|
||||
|
||||
__all__ = [Retriever, Document, Resource, RAGFlowProvider, build_retriever]
|
||||
@@ -0,0 +1,14 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from src.config.tools import SELECTED_RAG_PROVIDER, RAGProvider
|
||||
from src.rag.ragflow import RAGFlowProvider
|
||||
from src.rag.retriever import Retriever
|
||||
|
||||
|
||||
def build_retriever() -> Retriever | None:
|
||||
if SELECTED_RAG_PROVIDER == RAGProvider.RAGFLOW.value:
|
||||
return RAGFlowProvider()
|
||||
elif SELECTED_RAG_PROVIDER:
|
||||
raise ValueError(f"Unsupported RAG provider: {SELECTED_RAG_PROVIDER}")
|
||||
return None
|
||||
@@ -0,0 +1,133 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import os
|
||||
import requests
|
||||
from src.rag.retriever import Chunk, Document, Resource, Retriever
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
class RAGFlowProvider(Retriever):
|
||||
"""
|
||||
RAGFlowProvider is a provider that uses RAGFlow to retrieve documents.
|
||||
"""
|
||||
|
||||
api_url: str
|
||||
api_key: str
|
||||
page_size: int = 10
|
||||
|
||||
def __init__(self):
|
||||
api_url = os.getenv("RAGFLOW_API_URL")
|
||||
if not api_url:
|
||||
raise ValueError("RAGFLOW_API_URL is not set")
|
||||
self.api_url = api_url
|
||||
|
||||
api_key = os.getenv("RAGFLOW_API_KEY")
|
||||
if not api_key:
|
||||
raise ValueError("RAGFLOW_API_KEY is not set")
|
||||
self.api_key = api_key
|
||||
|
||||
page_size = os.getenv("RAGFLOW_PAGE_SIZE")
|
||||
if page_size:
|
||||
self.page_size = int(page_size)
|
||||
|
||||
def query_relevant_documents(
|
||||
self, query: str, resources: list[Resource] = []
|
||||
) -> list[Document]:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
dataset_ids: list[str] = []
|
||||
document_ids: list[str] = []
|
||||
|
||||
for resource in resources:
|
||||
dataset_id, document_id = parse_uri(resource.uri)
|
||||
dataset_ids.append(dataset_id)
|
||||
if document_id:
|
||||
document_ids.append(document_id)
|
||||
|
||||
payload = {
|
||||
"question": query,
|
||||
"dataset_ids": dataset_ids,
|
||||
"document_ids": document_ids,
|
||||
"page_size": self.page_size,
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{self.api_url}/api/v1/retrieval", headers=headers, json=payload
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to query documents: {response.text}")
|
||||
|
||||
result = response.json()
|
||||
data = result.get("data", {})
|
||||
doc_aggs = data.get("doc_aggs", [])
|
||||
docs: dict[str, Document] = {
|
||||
doc.get("doc_id"): Document(
|
||||
id=doc.get("doc_id"),
|
||||
title=doc.get("doc_name"),
|
||||
chunks=[],
|
||||
)
|
||||
for doc in doc_aggs
|
||||
}
|
||||
|
||||
for chunk in data.get("chunks", []):
|
||||
doc = docs.get(chunk.get("document_id"))
|
||||
if doc:
|
||||
doc.chunks.append(
|
||||
Chunk(
|
||||
content=chunk.get("content"),
|
||||
similarity=chunk.get("similarity"),
|
||||
)
|
||||
)
|
||||
|
||||
return list(docs.values())
|
||||
|
||||
def list_resources(self, query: str | None = None) -> list[Resource]:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
params = {}
|
||||
if query:
|
||||
params["name"] = query
|
||||
|
||||
response = requests.get(
|
||||
f"{self.api_url}/api/v1/datasets", headers=headers, params=params
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to list resources: {response.text}")
|
||||
|
||||
result = response.json()
|
||||
resources = []
|
||||
|
||||
for item in result.get("data", []):
|
||||
item = Resource(
|
||||
uri=f"rag://dataset/{item.get('id')}",
|
||||
title=item.get("name", ""),
|
||||
description=item.get("description", ""),
|
||||
)
|
||||
resources.append(item)
|
||||
|
||||
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
|
||||
|
||||
|
||||
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,80 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import abc
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class Chunk:
|
||||
content: str
|
||||
similarity: float
|
||||
|
||||
def __init__(self, content: str, similarity: float):
|
||||
self.content = content
|
||||
self.similarity = similarity
|
||||
|
||||
|
||||
class Document:
|
||||
"""
|
||||
Document is a class that represents a document.
|
||||
"""
|
||||
|
||||
id: str
|
||||
url: str | None = None
|
||||
title: str | None = None
|
||||
chunks: list[Chunk] = []
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
url: str | None = None,
|
||||
title: str | None = None,
|
||||
chunks: list[Chunk] = [],
|
||||
):
|
||||
self.id = id
|
||||
self.url = url
|
||||
self.title = title
|
||||
self.chunks = chunks
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = {
|
||||
"id": self.id,
|
||||
"content": "\n\n".join([chunk.content for chunk in self.chunks]),
|
||||
}
|
||||
if self.url:
|
||||
d["url"] = self.url
|
||||
if self.title:
|
||||
d["title"] = self.title
|
||||
return d
|
||||
|
||||
|
||||
class Resource(BaseModel):
|
||||
"""
|
||||
Resource is a class that represents a resource.
|
||||
"""
|
||||
|
||||
uri: str = Field(..., description="The URI of the resource")
|
||||
title: str = Field(..., description="The title of the resource")
|
||||
description: str | None = Field("", description="The description of the resource")
|
||||
|
||||
|
||||
class Retriever(abc.ABC):
|
||||
"""
|
||||
Define a RAG provider, which can be used to query documents and resources.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def list_resources(self, query: str | None = None) -> list[Resource]:
|
||||
"""
|
||||
List resources from the rag provider.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def query_relevant_documents(
|
||||
self, query: str, resources: list[Resource] = []
|
||||
) -> list[Document]:
|
||||
"""
|
||||
Query relevant documents from the resources.
|
||||
"""
|
||||
pass
|
||||
+108
-11
@@ -5,22 +5,27 @@ import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import List, cast
|
||||
from typing import Annotated, List, cast
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import Response, StreamingResponse
|
||||
from langchain_core.messages import AIMessageChunk, ToolMessage, BaseMessage
|
||||
from langgraph.types import Command
|
||||
|
||||
from src.config.report_style import ReportStyle
|
||||
from src.config.tools import SELECTED_RAG_PROVIDER
|
||||
from src.graph.builder import build_graph_with_memory
|
||||
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.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.rag.builder import build_retriever
|
||||
from src.rag.retriever import Resource
|
||||
from src.server.chat_request import (
|
||||
ChatMessage,
|
||||
ChatRequest,
|
||||
EnhancePromptRequest,
|
||||
GeneratePodcastRequest,
|
||||
GeneratePPTRequest,
|
||||
GenerateProseRequest,
|
||||
@@ -28,10 +33,19 @@ from src.server.chat_request import (
|
||||
)
|
||||
from src.server.mcp_request import MCPServerMetadataRequest, MCPServerMetadataResponse
|
||||
from src.server.mcp_utils import load_mcp_tools
|
||||
from src.server.rag_request import (
|
||||
RAGConfigResponse,
|
||||
RAGResourceRequest,
|
||||
RAGResourcesResponse,
|
||||
)
|
||||
from src.server.config_request import ConfigResponse
|
||||
from src.llms.llm import get_configured_llm_models
|
||||
from src.tools import VolcengineTTS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
INTERNAL_SERVER_ERROR_DETAIL = "Internal Server Error"
|
||||
|
||||
app = FastAPI(
|
||||
title="DeerFlow API",
|
||||
description="API for Deer",
|
||||
@@ -59,6 +73,7 @@ async def chat_stream(request: ChatRequest):
|
||||
_astream_workflow_generator(
|
||||
request.model_dump()["messages"],
|
||||
thread_id,
|
||||
request.resources,
|
||||
request.max_plan_iterations,
|
||||
request.max_step_num,
|
||||
request.max_search_results,
|
||||
@@ -66,21 +81,26 @@ async def chat_stream(request: ChatRequest):
|
||||
request.interrupt_feedback,
|
||||
request.mcp_settings,
|
||||
request.enable_background_investigation,
|
||||
request.report_style,
|
||||
request.enable_deep_thinking,
|
||||
),
|
||||
media_type="text/event-stream",
|
||||
)
|
||||
|
||||
|
||||
async def _astream_workflow_generator(
|
||||
messages: List[ChatMessage],
|
||||
messages: List[dict],
|
||||
thread_id: str,
|
||||
resources: List[Resource],
|
||||
max_plan_iterations: int,
|
||||
max_step_num: int,
|
||||
max_search_results: int,
|
||||
auto_accepted_plan: bool,
|
||||
interrupt_feedback: str,
|
||||
mcp_settings: dict,
|
||||
enable_background_investigation,
|
||||
enable_background_investigation: bool,
|
||||
report_style: ReportStyle,
|
||||
enable_deep_thinking: bool,
|
||||
):
|
||||
input_ = {
|
||||
"messages": messages,
|
||||
@@ -90,6 +110,7 @@ async def _astream_workflow_generator(
|
||||
"observations": [],
|
||||
"auto_accepted_plan": auto_accepted_plan,
|
||||
"enable_background_investigation": enable_background_investigation,
|
||||
"research_topic": messages[-1]["content"] if messages else "",
|
||||
}
|
||||
if not auto_accepted_plan and interrupt_feedback:
|
||||
resume_msg = f"[{interrupt_feedback}]"
|
||||
@@ -101,10 +122,13 @@ async def _astream_workflow_generator(
|
||||
input_,
|
||||
config={
|
||||
"thread_id": thread_id,
|
||||
"resources": resources,
|
||||
"max_plan_iterations": max_plan_iterations,
|
||||
"max_step_num": max_step_num,
|
||||
"max_search_results": max_search_results,
|
||||
"mcp_settings": mcp_settings,
|
||||
"report_style": report_style.value,
|
||||
"enable_deep_thinking": enable_deep_thinking,
|
||||
},
|
||||
stream_mode=["messages", "updates"],
|
||||
subgraphs=True,
|
||||
@@ -136,6 +160,10 @@ async def _astream_workflow_generator(
|
||||
"role": "assistant",
|
||||
"content": message_chunk.content,
|
||||
}
|
||||
if message_chunk.additional_kwargs.get("reasoning_content"):
|
||||
event_stream_message["reasoning_content"] = message_chunk.additional_kwargs[
|
||||
"reasoning_content"
|
||||
]
|
||||
if message_chunk.response_metadata.get("finish_reason"):
|
||||
event_stream_message["finish_reason"] = message_chunk.response_metadata.get(
|
||||
"finish_reason"
|
||||
@@ -223,7 +251,7 @@ async def text_to_speech(request: TTSRequest):
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in TTS endpoint: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR_DETAIL)
|
||||
|
||||
|
||||
@app.post("/api/podcast/generate")
|
||||
@@ -237,7 +265,7 @@ async def generate_podcast(request: GeneratePodcastRequest):
|
||||
return Response(content=audio_bytes, media_type="audio/mp3")
|
||||
except Exception as e:
|
||||
logger.exception(f"Error occurred during podcast generation: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR_DETAIL)
|
||||
|
||||
|
||||
@app.post("/api/ppt/generate")
|
||||
@@ -256,13 +284,14 @@ async def generate_ppt(request: GeneratePPTRequest):
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error occurred during ppt generation: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR_DETAIL)
|
||||
|
||||
|
||||
@app.post("/api/prose/generate")
|
||||
async def generate_prose(request: GenerateProseRequest):
|
||||
try:
|
||||
logger.info(f"Generating prose for prompt: {request.prompt}")
|
||||
sanitized_prompt = request.prompt.replace("\r\n", "").replace("\n", "")
|
||||
logger.info(f"Generating prose for prompt: {sanitized_prompt}")
|
||||
workflow = build_prose_graph()
|
||||
events = workflow.astream(
|
||||
{
|
||||
@@ -279,7 +308,51 @@ async def generate_prose(request: GenerateProseRequest):
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error occurred during prose generation: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR_DETAIL)
|
||||
|
||||
|
||||
@app.post("/api/prompt/enhance")
|
||||
async def enhance_prompt(request: EnhancePromptRequest):
|
||||
try:
|
||||
sanitized_prompt = request.prompt.replace("\r\n", "").replace("\n", "")
|
||||
logger.info(f"Enhancing prompt: {sanitized_prompt}")
|
||||
|
||||
# Convert string report_style to ReportStyle enum
|
||||
report_style = None
|
||||
if request.report_style:
|
||||
try:
|
||||
# Handle both uppercase and lowercase input
|
||||
style_mapping = {
|
||||
"ACADEMIC": ReportStyle.ACADEMIC,
|
||||
"POPULAR_SCIENCE": ReportStyle.POPULAR_SCIENCE,
|
||||
"NEWS": ReportStyle.NEWS,
|
||||
"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(
|
||||
request.report_style, ReportStyle.ACADEMIC
|
||||
)
|
||||
except Exception:
|
||||
# If invalid style, default to ACADEMIC
|
||||
report_style = ReportStyle.ACADEMIC
|
||||
else:
|
||||
report_style = ReportStyle.ACADEMIC
|
||||
|
||||
workflow = build_prompt_enhancer_graph()
|
||||
final_state = workflow.invoke(
|
||||
{
|
||||
"prompt": request.prompt,
|
||||
"context": request.context,
|
||||
"report_style": report_style,
|
||||
}
|
||||
)
|
||||
return {"result": final_state["output"]}
|
||||
except Exception as e:
|
||||
logger.exception(f"Error occurred during prompt enhancement: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR_DETAIL)
|
||||
|
||||
|
||||
@app.post("/api/mcp/server/metadata", response_model=MCPServerMetadataResponse)
|
||||
@@ -317,5 +390,29 @@ async def mcp_server_metadata(request: MCPServerMetadataRequest):
|
||||
except Exception as e:
|
||||
if not isinstance(e, HTTPException):
|
||||
logger.exception(f"Error in MCP server metadata endpoint: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=INTERNAL_SERVER_ERROR_DETAIL)
|
||||
raise
|
||||
|
||||
|
||||
@app.get("/api/rag/config", response_model=RAGConfigResponse)
|
||||
async def rag_config():
|
||||
"""Get the config of the RAG."""
|
||||
return RAGConfigResponse(provider=SELECTED_RAG_PROVIDER)
|
||||
|
||||
|
||||
@app.get("/api/rag/resources", response_model=RAGResourcesResponse)
|
||||
async def rag_resources(request: Annotated[RAGResourceRequest, Query()]):
|
||||
"""Get the resources of the RAG."""
|
||||
retriever = build_retriever()
|
||||
if retriever:
|
||||
return RAGResourcesResponse(resources=retriever.list_resources(request.query))
|
||||
return RAGResourcesResponse(resources=[])
|
||||
|
||||
|
||||
@app.get("/api/config", response_model=ConfigResponse)
|
||||
async def config():
|
||||
"""Get the config of the server."""
|
||||
return ConfigResponse(
|
||||
rag=RAGConfigResponse(provider=SELECTED_RAG_PROVIDER),
|
||||
models=get_configured_llm_models(),
|
||||
)
|
||||
|
||||
@@ -5,6 +5,9 @@ from typing import List, Optional, Union
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from src.rag.retriever import Resource
|
||||
from src.config.report_style import ReportStyle
|
||||
|
||||
|
||||
class ContentItem(BaseModel):
|
||||
type: str = Field(..., description="The type of content (text, image, etc.)")
|
||||
@@ -28,6 +31,9 @@ class ChatRequest(BaseModel):
|
||||
messages: Optional[List[ChatMessage]] = Field(
|
||||
[], description="History of messages between the user and the assistant"
|
||||
)
|
||||
resources: Optional[List[Resource]] = Field(
|
||||
[], description="Resources to be used for the research"
|
||||
)
|
||||
debug: Optional[bool] = Field(False, description="Whether to enable debug logging")
|
||||
thread_id: Optional[str] = Field(
|
||||
"__default__", description="A specific conversation identifier"
|
||||
@@ -53,6 +59,12 @@ class ChatRequest(BaseModel):
|
||||
enable_background_investigation: Optional[bool] = Field(
|
||||
True, description="Whether to get background investigation before plan"
|
||||
)
|
||||
report_style: Optional[ReportStyle] = Field(
|
||||
ReportStyle.ACADEMIC, description="The style of the report"
|
||||
)
|
||||
enable_deep_thinking: Optional[bool] = Field(
|
||||
False, description="Whether to enable deep thinking"
|
||||
)
|
||||
|
||||
|
||||
class TTSRequest(BaseModel):
|
||||
@@ -85,3 +97,13 @@ class GenerateProseRequest(BaseModel):
|
||||
command: Optional[str] = Field(
|
||||
"", description="The user custom command of the prose writer"
|
||||
)
|
||||
|
||||
|
||||
class EnhancePromptRequest(BaseModel):
|
||||
prompt: str = Field(..., description="The original prompt to enhance")
|
||||
context: Optional[str] = Field(
|
||||
"", description="Additional context about the intended use"
|
||||
)
|
||||
report_style: Optional[str] = Field(
|
||||
"academic", description="The style of the report"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from src.server.rag_request import RAGConfigResponse
|
||||
|
||||
|
||||
class ConfigResponse(BaseModel):
|
||||
"""Response model for server config."""
|
||||
|
||||
rag: RAGConfigResponse = Field(..., description="The config of the RAG")
|
||||
models: dict[str, list[str]] = Field(..., description="The configured models")
|
||||
@@ -0,0 +1,28 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from src.rag.retriever import Resource
|
||||
|
||||
|
||||
class RAGConfigResponse(BaseModel):
|
||||
"""Response model for RAG config."""
|
||||
|
||||
provider: str | None = Field(
|
||||
None, description="The provider of the RAG, default is ragflow"
|
||||
)
|
||||
|
||||
|
||||
class RAGResourceRequest(BaseModel):
|
||||
"""Request model for RAG resource."""
|
||||
|
||||
query: str | None = Field(
|
||||
None, description="The query of the resource need to be searched"
|
||||
)
|
||||
|
||||
|
||||
class RAGResourcesResponse(BaseModel):
|
||||
"""Response model for RAG resources."""
|
||||
|
||||
resources: list[Resource] = Field(..., description="The resources of the RAG")
|
||||
@@ -5,6 +5,7 @@ import os
|
||||
|
||||
from .crawl import crawl_tool
|
||||
from .python_repl import python_repl_tool
|
||||
from .retriever import get_retriever_tool
|
||||
from .search import get_web_search_tool
|
||||
from .tts import VolcengineTTS
|
||||
|
||||
@@ -12,5 +13,6 @@ __all__ = [
|
||||
"crawl_tool",
|
||||
"python_repl_tool",
|
||||
"get_web_search_tool",
|
||||
"get_retriever_tool",
|
||||
"VolcengineTTS",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import logging
|
||||
from typing import List, Optional, Type
|
||||
from langchain_core.tools import BaseTool
|
||||
from langchain_core.callbacks import (
|
||||
AsyncCallbackManagerForToolRun,
|
||||
CallbackManagerForToolRun,
|
||||
)
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from src.config.tools import SELECTED_RAG_PROVIDER
|
||||
from src.rag import Document, Retriever, Resource, build_retriever
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RetrieverInput(BaseModel):
|
||||
keywords: str = Field(description="search keywords to look up")
|
||||
|
||||
|
||||
class RetrieverTool(BaseTool):
|
||||
name: str = "local_search_tool"
|
||||
description: str = (
|
||||
"Useful for retrieving information from the file with `rag://` uri prefix, it should be higher priority than the web search or writing code. Input should be a search keywords."
|
||||
)
|
||||
args_schema: Type[BaseModel] = RetrieverInput
|
||||
|
||||
retriever: Retriever = Field(default_factory=Retriever)
|
||||
resources: list[Resource] = Field(default_factory=list)
|
||||
|
||||
def _run(
|
||||
self,
|
||||
keywords: str,
|
||||
run_manager: Optional[CallbackManagerForToolRun] = None,
|
||||
) -> list[Document]:
|
||||
logger.info(
|
||||
f"Retriever tool query: {keywords}", extra={"resources": self.resources}
|
||||
)
|
||||
documents = self.retriever.query_relevant_documents(keywords, self.resources)
|
||||
if not documents:
|
||||
return "No results found from the local knowledge base."
|
||||
return [doc.to_dict() for doc in documents]
|
||||
|
||||
async def _arun(
|
||||
self,
|
||||
keywords: str,
|
||||
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
|
||||
) -> list[Document]:
|
||||
return self._run(keywords, run_manager.get_sync())
|
||||
|
||||
|
||||
def get_retriever_tool(resources: List[Resource]) -> RetrieverTool | None:
|
||||
if not resources:
|
||||
return None
|
||||
logger.info(f"create retriever tool: {SELECTED_RAG_PROVIDER}")
|
||||
retriever = build_retriever()
|
||||
|
||||
if not retriever:
|
||||
return None
|
||||
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("三打白骨精"))
|
||||
+6
-2
@@ -61,5 +61,9 @@ def get_web_search_tool(max_search_results: int):
|
||||
if __name__ == "__main__":
|
||||
results = LoggedDuckDuckGoSearch(
|
||||
name="web_search", max_results=3, output_format="list"
|
||||
).invoke("cute panda")
|
||||
print(json.dumps(results, indent=2, ensure_ascii=False))
|
||||
)
|
||||
print(results.name)
|
||||
print(results.description)
|
||||
print(results.args)
|
||||
# .invoke("cute panda")
|
||||
# print(json.dumps(results, indent=2, ensure_ascii=False))
|
||||
|
||||
@@ -70,7 +70,7 @@ class EnhancedTavilySearchAPIWrapper(OriginalTavilySearchAPIWrapper):
|
||||
"include_images": include_images,
|
||||
"include_image_descriptions": include_image_descriptions,
|
||||
}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with session.post(f"{TAVILY_API_URL}/search", json=params) as res:
|
||||
if res.status == 200:
|
||||
data = await res.text()
|
||||
|
||||
+2
-1
@@ -102,7 +102,8 @@ class VolcengineTTS:
|
||||
}
|
||||
|
||||
try:
|
||||
logger.debug(f"Sending TTS request for text: {text[:50]}...")
|
||||
sanitized_text = text.replace("\r\n", "").replace("\n", "")
|
||||
logger.debug(f"Sending TTS request for text: {sanitized_text[:50]}...")
|
||||
response = requests.post(
|
||||
self.api_url, json.dumps(request_json), headers=self.header
|
||||
)
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import json
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# 在这里 mock 掉 get_llm_by_type,避免 ValueError
|
||||
with patch("src.llms.llm.get_llm_by_type", return_value=MagicMock()):
|
||||
from langgraph.types import Command
|
||||
from src.graph.nodes import background_investigation_node
|
||||
from src.config import SearchEngine
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
# Mock data
|
||||
MOCK_SEARCH_RESULTS = [
|
||||
{"title": "Test Title 1", "content": "Test Content 1"},
|
||||
{"title": "Test Title 2", "content": "Test Content 2"},
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_state():
|
||||
return {
|
||||
"messages": [HumanMessage(content="test query")],
|
||||
"research_topic": "test query",
|
||||
"background_investigation_results": None,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_configurable():
|
||||
mock = MagicMock()
|
||||
mock.max_search_results = 5
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config():
|
||||
# 你可以根据实际需要返回一个 MagicMock 或 dict
|
||||
return MagicMock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def patch_config_from_runnable_config(mock_configurable):
|
||||
with patch(
|
||||
"src.graph.nodes.Configuration.from_runnable_config",
|
||||
return_value=mock_configurable,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_tavily_search():
|
||||
with patch("src.graph.nodes.LoggedTavilySearch") as mock:
|
||||
instance = mock.return_value
|
||||
instance.invoke.return_value = [
|
||||
{"title": "Test Title 1", "content": "Test Content 1"},
|
||||
{"title": "Test Title 2", "content": "Test Content 2"},
|
||||
]
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_web_search_tool():
|
||||
with patch("src.graph.nodes.get_web_search_tool") as mock:
|
||||
instance = mock.return_value
|
||||
instance.invoke.return_value = [
|
||||
{"title": "Test Title 1", "content": "Test Content 1"},
|
||||
{"title": "Test Title 2", "content": "Test Content 2"},
|
||||
]
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.mark.parametrize("search_engine", [SearchEngine.TAVILY.value, "other"])
|
||||
def test_background_investigation_node_tavily(
|
||||
mock_state,
|
||||
mock_tavily_search,
|
||||
mock_web_search_tool,
|
||||
search_engine,
|
||||
patch_config_from_runnable_config,
|
||||
mock_config,
|
||||
):
|
||||
"""Test background_investigation_node with Tavily search engine"""
|
||||
with patch("src.graph.nodes.SELECTED_SEARCH_ENGINE", search_engine):
|
||||
result = background_investigation_node(mock_state, mock_config)
|
||||
|
||||
# Verify the result structure
|
||||
assert isinstance(result, dict)
|
||||
|
||||
# Verify the update contains background_investigation_results
|
||||
assert "background_investigation_results" in result
|
||||
|
||||
# Parse and verify the JSON content
|
||||
results = result["background_investigation_results"]
|
||||
|
||||
if search_engine == SearchEngine.TAVILY.value:
|
||||
mock_tavily_search.return_value.invoke.assert_called_once_with("test query")
|
||||
assert (
|
||||
results
|
||||
== "## Test Title 1\n\nTest Content 1\n\n## Test Title 2\n\nTest Content 2"
|
||||
)
|
||||
else:
|
||||
mock_web_search_tool.return_value.invoke.assert_called_once_with(
|
||||
"test query"
|
||||
)
|
||||
assert len(json.loads(results)) == 2
|
||||
|
||||
|
||||
def test_background_investigation_node_malformed_response(
|
||||
mock_state, mock_tavily_search, patch_config_from_runnable_config, mock_config
|
||||
):
|
||||
"""Test background_investigation_node with malformed Tavily response"""
|
||||
with patch("src.graph.nodes.SELECTED_SEARCH_ENGINE", SearchEngine.TAVILY.value):
|
||||
# Mock a malformed response
|
||||
mock_tavily_search.return_value.invoke.return_value = "invalid response"
|
||||
|
||||
result = background_investigation_node(mock_state, mock_config)
|
||||
|
||||
# Verify the result structure
|
||||
assert isinstance(result, dict)
|
||||
|
||||
# Verify the update contains background_investigation_results
|
||||
assert "background_investigation_results" in result
|
||||
|
||||
# Parse and verify the JSON content
|
||||
results = result["background_investigation_results"]
|
||||
assert json.loads(results) is None
|
||||
@@ -106,3 +106,40 @@ def test_current_time_format():
|
||||
assert any(
|
||||
line.strip().startswith("CURRENT_TIME:") for line in system_content.split("\n")
|
||||
)
|
||||
|
||||
|
||||
def test_apply_prompt_template_reporter():
|
||||
"""Test reporter template rendering with different styles and locale"""
|
||||
|
||||
test_state_news = {
|
||||
"messages": [],
|
||||
"task": "test reporter task",
|
||||
"workspace_context": "test reporter context",
|
||||
"report_style": "news",
|
||||
"locale": "en-US",
|
||||
}
|
||||
messages_news = apply_prompt_template("reporter", test_state_news)
|
||||
system_content_news = messages_news[0]["content"]
|
||||
assert "NBC News" in system_content_news
|
||||
|
||||
test_state_social_media_en = {
|
||||
"messages": [],
|
||||
"task": "test reporter task",
|
||||
"workspace_context": "test reporter context",
|
||||
"report_style": "social_media",
|
||||
"locale": "en-US",
|
||||
}
|
||||
messages_default = apply_prompt_template("reporter", test_state_social_media_en)
|
||||
system_content_default = messages_default[0]["content"]
|
||||
assert "Twitter/X" in system_content_default
|
||||
|
||||
test_state_social_media_cn = {
|
||||
"messages": [],
|
||||
"task": "test reporter task",
|
||||
"workspace_context": "test reporter context",
|
||||
"report_style": "social_media",
|
||||
"locale": "zh-CN",
|
||||
}
|
||||
messages_cn = apply_prompt_template("reporter", test_state_social_media_cn)
|
||||
system_content_cn = messages_cn[0]["content"]
|
||||
assert "小红书" in system_content_cn
|
||||
|
||||
+6
-3
@@ -1,3 +1,6 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
import os
|
||||
@@ -14,8 +17,8 @@ class StepType:
|
||||
|
||||
|
||||
class Step:
|
||||
def __init__(self, need_web_search, title, description, step_type):
|
||||
self.need_web_search = need_web_search
|
||||
def __init__(self, need_search, title, description, step_type):
|
||||
self.need_search = need_search
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.step_type = step_type
|
||||
@@ -90,7 +93,7 @@ def test_state_initialization():
|
||||
def test_state_with_custom_values():
|
||||
"""Test that State can be initialized with custom values."""
|
||||
test_step = Step(
|
||||
need_web_search=True,
|
||||
need_search=True,
|
||||
title="Test Step",
|
||||
description="Step description",
|
||||
step_type=StepType.RESEARCH,
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import os
|
||||
import pytest
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
import builtins
|
||||
import importlib
|
||||
from src.config.configuration import Configuration
|
||||
|
||||
# Patch sys.path so relative import works
|
||||
|
||||
# Patch Resource for import
|
||||
mock_resource = type("Resource", (), {})
|
||||
|
||||
# Patch src.rag.retriever.Resource for import
|
||||
|
||||
module_name = "src.rag.retriever"
|
||||
if module_name not in sys.modules:
|
||||
retriever_mod = types.ModuleType(module_name)
|
||||
retriever_mod.Resource = mock_resource
|
||||
sys.modules[module_name] = retriever_mod
|
||||
|
||||
# Relative import of Configuration
|
||||
|
||||
|
||||
def test_default_configuration():
|
||||
config = Configuration()
|
||||
assert config.resources == []
|
||||
assert config.max_plan_iterations == 1
|
||||
assert config.max_step_num == 3
|
||||
assert config.max_search_results == 3
|
||||
assert config.mcp_settings is None
|
||||
|
||||
|
||||
def test_from_runnable_config_with_config_dict(monkeypatch):
|
||||
config_dict = {
|
||||
"configurable": {
|
||||
"max_plan_iterations": 5,
|
||||
"max_step_num": 7,
|
||||
"max_search_results": 10,
|
||||
"mcp_settings": {"foo": "bar"},
|
||||
}
|
||||
}
|
||||
config = Configuration.from_runnable_config(config_dict)
|
||||
assert config.max_plan_iterations == 5
|
||||
assert config.max_step_num == 7
|
||||
assert config.max_search_results == 10
|
||||
assert config.mcp_settings == {"foo": "bar"}
|
||||
|
||||
|
||||
def test_from_runnable_config_with_env_override(monkeypatch):
|
||||
monkeypatch.setenv("MAX_PLAN_ITERATIONS", "9")
|
||||
monkeypatch.setenv("MAX_STEP_NUM", "11")
|
||||
config_dict = {
|
||||
"configurable": {
|
||||
"max_plan_iterations": 2,
|
||||
"max_step_num": 3,
|
||||
"max_search_results": 4,
|
||||
}
|
||||
}
|
||||
config = Configuration.from_runnable_config(config_dict)
|
||||
# Environment variables take precedence and are strings
|
||||
assert config.max_plan_iterations == "9"
|
||||
assert config.max_step_num == "11"
|
||||
assert config.max_search_results == 4 # not overridden
|
||||
# Clean up
|
||||
monkeypatch.delenv("MAX_PLAN_ITERATIONS")
|
||||
monkeypatch.delenv("MAX_STEP_NUM")
|
||||
|
||||
|
||||
def test_from_runnable_config_with_none_and_falsy(monkeypatch):
|
||||
config_dict = {
|
||||
"configurable": {
|
||||
"max_plan_iterations": None,
|
||||
"max_step_num": 0, # falsy, should be skipped
|
||||
"max_search_results": "",
|
||||
}
|
||||
}
|
||||
config = Configuration.from_runnable_config(config_dict)
|
||||
# Should fall back to defaults for skipped/falsy values
|
||||
assert config.max_plan_iterations == 1
|
||||
assert config.max_step_num == 3
|
||||
assert config.max_search_results == 3
|
||||
|
||||
|
||||
def test_from_runnable_config_with_no_config():
|
||||
config = Configuration.from_runnable_config()
|
||||
assert config.max_plan_iterations == 1
|
||||
assert config.max_step_num == 3
|
||||
assert config.max_search_results == 3
|
||||
assert config.resources == []
|
||||
assert config.mcp_settings is None
|
||||
@@ -0,0 +1,83 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import yaml
|
||||
import pytest
|
||||
from src.config.loader import load_yaml_config, process_dict, replace_env_vars
|
||||
|
||||
|
||||
def test_replace_env_vars_with_env(monkeypatch):
|
||||
monkeypatch.setenv("TEST_ENV", "env_value")
|
||||
assert replace_env_vars("$TEST_ENV") == "env_value"
|
||||
|
||||
|
||||
def test_replace_env_vars_without_env(monkeypatch):
|
||||
monkeypatch.delenv("NOT_SET_ENV", raising=False)
|
||||
assert replace_env_vars("$NOT_SET_ENV") == "NOT_SET_ENV"
|
||||
|
||||
|
||||
def test_replace_env_vars_non_string():
|
||||
assert replace_env_vars(123) == 123
|
||||
|
||||
|
||||
def test_replace_env_vars_regular_string():
|
||||
assert replace_env_vars("no_env") == "no_env"
|
||||
|
||||
|
||||
def test_process_dict_nested(monkeypatch):
|
||||
monkeypatch.setenv("FOO", "bar")
|
||||
config = {"a": "$FOO", "b": {"c": "$FOO", "d": 42, "e": "$NOT_SET_ENV"}}
|
||||
processed = process_dict(config)
|
||||
assert processed["a"] == "bar"
|
||||
assert processed["b"]["c"] == "bar"
|
||||
assert processed["b"]["d"] == 42
|
||||
assert processed["b"]["e"] == "NOT_SET_ENV"
|
||||
|
||||
|
||||
def test_process_dict_empty():
|
||||
assert process_dict({}) == {}
|
||||
|
||||
|
||||
def test_load_yaml_config_file_not_exist():
|
||||
assert load_yaml_config("non_existent_file.yaml") == {}
|
||||
|
||||
|
||||
def test_load_yaml_config(monkeypatch):
|
||||
monkeypatch.setenv("MY_ENV", "my_value")
|
||||
yaml_content = """
|
||||
key1: value1
|
||||
key2: $MY_ENV
|
||||
nested:
|
||||
key3: $MY_ENV
|
||||
key4: 123
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile("w+", delete=False) as tmp:
|
||||
tmp.write(yaml_content)
|
||||
tmp_path = tmp.name
|
||||
|
||||
try:
|
||||
config = load_yaml_config(tmp_path)
|
||||
assert config["key1"] == "value1"
|
||||
assert config["key2"] == "my_value"
|
||||
assert config["nested"]["key3"] == "my_value"
|
||||
assert config["nested"]["key4"] == 123
|
||||
finally:
|
||||
os.remove(tmp_path)
|
||||
|
||||
|
||||
def test_load_yaml_config_cache(monkeypatch):
|
||||
monkeypatch.setenv("CACHE_ENV", "cache_value")
|
||||
yaml_content = "foo: $CACHE_ENV"
|
||||
with tempfile.NamedTemporaryFile("w+", delete=False) as tmp:
|
||||
tmp.write(yaml_content)
|
||||
tmp_path = tmp.name
|
||||
|
||||
try:
|
||||
config1 = load_yaml_config(tmp_path)
|
||||
config2 = load_yaml_config(tmp_path)
|
||||
assert config1 is config2 # Should be cached (same object)
|
||||
assert config1["foo"] == "cache_value"
|
||||
finally:
|
||||
os.remove(tmp_path)
|
||||
@@ -0,0 +1,74 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
import pytest
|
||||
from src.crawler.article import Article
|
||||
|
||||
|
||||
class DummyMarkdownify:
|
||||
"""A dummy markdownify replacement for patching if needed."""
|
||||
|
||||
@staticmethod
|
||||
def markdownify(html):
|
||||
return html
|
||||
|
||||
|
||||
def test_to_markdown_includes_title(monkeypatch):
|
||||
article = Article("Test Title", "<p>Hello <b>world</b>!</p>")
|
||||
result = article.to_markdown(including_title=True)
|
||||
assert result.startswith("# Test Title")
|
||||
assert "Hello" in result
|
||||
|
||||
|
||||
def test_to_markdown_excludes_title():
|
||||
article = Article("Test Title", "<p>Hello <b>world</b>!</p>")
|
||||
result = article.to_markdown(including_title=False)
|
||||
assert not result.startswith("# Test Title")
|
||||
assert "Hello" in result
|
||||
|
||||
|
||||
def test_to_message_with_text_only():
|
||||
article = Article("Test Title", "<p>Hello world!</p>")
|
||||
article.url = "https://example.com/"
|
||||
result = article.to_message()
|
||||
assert isinstance(result, list)
|
||||
assert any(item["type"] == "text" for item in result)
|
||||
assert all("type" in item for item in result)
|
||||
|
||||
|
||||
def test_to_message_with_image(monkeypatch):
|
||||
html = '<p>Intro</p><img src="img/pic.png"/>'
|
||||
article = Article("Title", html)
|
||||
article.url = "https://host.com/path/"
|
||||
# The markdownify library will convert <img> to markdown image syntax
|
||||
result = article.to_message()
|
||||
# Should have both text and image_url types
|
||||
types = [item["type"] for item in result]
|
||||
assert "image_url" in types
|
||||
assert "text" in types
|
||||
# Check that the image_url is correctly joined
|
||||
image_items = [item for item in result if item["type"] == "image_url"]
|
||||
assert image_items
|
||||
assert image_items[0]["image_url"]["url"] == "https://host.com/path/img/pic.png"
|
||||
|
||||
|
||||
def test_to_message_multiple_images():
|
||||
html = '<p>Start</p><img src="a.png"/><p>Mid</p><img src="b.jpg"/>End'
|
||||
article = Article("Title", html)
|
||||
article.url = "http://x/"
|
||||
result = article.to_message()
|
||||
image_urls = [
|
||||
item["image_url"]["url"] for item in result if item["type"] == "image_url"
|
||||
]
|
||||
assert "http://x/a.png" in image_urls
|
||||
assert "http://x/b.jpg" in image_urls
|
||||
text_items = [item for item in result if item["type"] == "text"]
|
||||
assert any("Start" in item["text"] for item in text_items)
|
||||
assert any("Mid" in item["text"] for item in text_items)
|
||||
|
||||
|
||||
def test_to_message_handles_empty_html():
|
||||
article = Article("Empty", "")
|
||||
article.url = "http://test/"
|
||||
result = article.to_message()
|
||||
assert isinstance(result, list)
|
||||
assert result[0]["type"] == "text"
|
||||
@@ -0,0 +1,72 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import pytest
|
||||
import src.crawler as crawler_module
|
||||
from src.crawler import Crawler
|
||||
|
||||
|
||||
def test_crawler_sets_article_url(monkeypatch):
|
||||
"""Test that the crawler sets the article.url field correctly."""
|
||||
|
||||
class DummyArticle:
|
||||
def __init__(self):
|
||||
self.url = None
|
||||
|
||||
def to_markdown(self):
|
||||
return "# Dummy"
|
||||
|
||||
class DummyJinaClient:
|
||||
def crawl(self, url, return_format=None):
|
||||
return "<html>dummy</html>"
|
||||
|
||||
class DummyReadabilityExtractor:
|
||||
def extract_article(self, html):
|
||||
return DummyArticle()
|
||||
|
||||
monkeypatch.setattr("src.crawler.crawler.JinaClient", DummyJinaClient)
|
||||
monkeypatch.setattr(
|
||||
"src.crawler.crawler.ReadabilityExtractor", DummyReadabilityExtractor
|
||||
)
|
||||
|
||||
crawler = crawler_module.Crawler()
|
||||
url = "http://example.com"
|
||||
article = crawler.crawl(url)
|
||||
assert article.url == url
|
||||
assert article.to_markdown() == "# Dummy"
|
||||
|
||||
|
||||
def test_crawler_calls_dependencies(monkeypatch):
|
||||
"""Test that Crawler calls JinaClient.crawl and ReadabilityExtractor.extract_article."""
|
||||
calls = {}
|
||||
|
||||
class DummyJinaClient:
|
||||
def crawl(self, url, return_format=None):
|
||||
calls["jina"] = (url, return_format)
|
||||
return "<html>dummy</html>"
|
||||
|
||||
class DummyReadabilityExtractor:
|
||||
def extract_article(self, html):
|
||||
calls["extractor"] = html
|
||||
|
||||
class DummyArticle:
|
||||
url = None
|
||||
|
||||
def to_markdown(self):
|
||||
return "# Dummy"
|
||||
|
||||
return DummyArticle()
|
||||
|
||||
monkeypatch.setattr("src.crawler.crawler.JinaClient", DummyJinaClient)
|
||||
monkeypatch.setattr(
|
||||
"src.crawler.crawler.ReadabilityExtractor", DummyReadabilityExtractor
|
||||
)
|
||||
|
||||
crawler = crawler_module.Crawler()
|
||||
url = "http://example.com"
|
||||
crawler.crawl(url)
|
||||
assert "jina" in calls
|
||||
assert calls["jina"][0] == url
|
||||
assert calls["jina"][1] == "html"
|
||||
assert "extractor" in calls
|
||||
assert calls["extractor"] == "<html>dummy</html>"
|
||||
@@ -0,0 +1,2 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
@@ -0,0 +1,2 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
@@ -0,0 +1,156 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from src.prompt_enhancer.graph.builder import build_graph
|
||||
from src.prompt_enhancer.graph.state import PromptEnhancerState
|
||||
from src.config.report_style import ReportStyle
|
||||
|
||||
|
||||
class TestBuildGraph:
|
||||
"""Test cases for build_graph function."""
|
||||
|
||||
@patch("src.prompt_enhancer.graph.builder.StateGraph")
|
||||
def test_build_graph_structure(self, mock_state_graph):
|
||||
"""Test that build_graph creates the correct graph structure."""
|
||||
mock_builder = MagicMock()
|
||||
mock_compiled_graph = MagicMock()
|
||||
|
||||
mock_state_graph.return_value = mock_builder
|
||||
mock_builder.compile.return_value = mock_compiled_graph
|
||||
|
||||
result = build_graph()
|
||||
|
||||
# Verify StateGraph was created with correct state type
|
||||
mock_state_graph.assert_called_once_with(PromptEnhancerState)
|
||||
|
||||
# Verify entry point was set
|
||||
mock_builder.set_entry_point.assert_called_once_with("enhancer")
|
||||
|
||||
# Verify finish point was set
|
||||
mock_builder.set_finish_point.assert_called_once_with("enhancer")
|
||||
|
||||
# Verify graph was compiled
|
||||
mock_builder.compile.assert_called_once()
|
||||
|
||||
# Verify return value
|
||||
assert result == mock_compiled_graph
|
||||
|
||||
@patch("src.prompt_enhancer.graph.builder.StateGraph")
|
||||
@patch("src.prompt_enhancer.graph.builder.prompt_enhancer_node")
|
||||
def test_build_graph_node_function(self, mock_enhancer_node, mock_state_graph):
|
||||
"""Test that the correct node function is added to the graph."""
|
||||
mock_builder = MagicMock()
|
||||
mock_compiled_graph = MagicMock()
|
||||
|
||||
mock_state_graph.return_value = mock_builder
|
||||
mock_builder.compile.return_value = mock_compiled_graph
|
||||
|
||||
result = build_graph()
|
||||
|
||||
# Verify the correct node function was added
|
||||
mock_builder.add_node.assert_called_once_with("enhancer", mock_enhancer_node)
|
||||
|
||||
def test_build_graph_returns_compiled_graph(self):
|
||||
"""Test that build_graph returns a compiled graph object."""
|
||||
with patch("src.prompt_enhancer.graph.builder.StateGraph") as mock_state_graph:
|
||||
mock_builder = MagicMock()
|
||||
mock_compiled_graph = MagicMock()
|
||||
|
||||
mock_state_graph.return_value = mock_builder
|
||||
mock_builder.compile.return_value = mock_compiled_graph
|
||||
|
||||
result = build_graph()
|
||||
|
||||
assert result is mock_compiled_graph
|
||||
|
||||
@patch("src.prompt_enhancer.graph.builder.StateGraph")
|
||||
def test_build_graph_call_sequence(self, mock_state_graph):
|
||||
"""Test that build_graph calls methods in the correct sequence."""
|
||||
mock_builder = MagicMock()
|
||||
mock_compiled_graph = MagicMock()
|
||||
|
||||
mock_state_graph.return_value = mock_builder
|
||||
mock_builder.compile.return_value = mock_compiled_graph
|
||||
|
||||
# Track call order
|
||||
call_order = []
|
||||
|
||||
def track_add_node(*args, **kwargs):
|
||||
call_order.append("add_node")
|
||||
|
||||
def track_set_entry_point(*args, **kwargs):
|
||||
call_order.append("set_entry_point")
|
||||
|
||||
def track_set_finish_point(*args, **kwargs):
|
||||
call_order.append("set_finish_point")
|
||||
|
||||
def track_compile(*args, **kwargs):
|
||||
call_order.append("compile")
|
||||
return mock_compiled_graph
|
||||
|
||||
mock_builder.add_node.side_effect = track_add_node
|
||||
mock_builder.set_entry_point.side_effect = track_set_entry_point
|
||||
mock_builder.set_finish_point.side_effect = track_set_finish_point
|
||||
mock_builder.compile.side_effect = track_compile
|
||||
|
||||
build_graph()
|
||||
|
||||
# Verify the correct call sequence
|
||||
expected_order = ["add_node", "set_entry_point", "set_finish_point", "compile"]
|
||||
assert call_order == expected_order
|
||||
|
||||
def test_build_graph_integration(self):
|
||||
"""Integration test to verify the graph can be built without mocking."""
|
||||
# This test verifies that all imports and dependencies are correct
|
||||
try:
|
||||
graph = build_graph()
|
||||
assert graph is not None
|
||||
# The graph should be a compiled LangGraph object
|
||||
assert hasattr(graph, "invoke") or hasattr(graph, "stream")
|
||||
except ImportError as e:
|
||||
pytest.skip(f"Skipping integration test due to missing dependencies: {e}")
|
||||
except Exception as e:
|
||||
# If there are configuration issues (like missing LLM config),
|
||||
# we still consider the test successful if the graph structure is built
|
||||
if "LLM" in str(e) or "configuration" in str(e).lower():
|
||||
pytest.skip(
|
||||
f"Skipping integration test due to configuration issues: {e}"
|
||||
)
|
||||
else:
|
||||
raise
|
||||
|
||||
@patch("src.prompt_enhancer.graph.builder.StateGraph")
|
||||
def test_build_graph_single_node_workflow(self, mock_state_graph):
|
||||
"""Test that the graph is configured as a single-node workflow."""
|
||||
mock_builder = MagicMock()
|
||||
mock_compiled_graph = MagicMock()
|
||||
|
||||
mock_state_graph.return_value = mock_builder
|
||||
mock_builder.compile.return_value = mock_compiled_graph
|
||||
|
||||
build_graph()
|
||||
|
||||
# Verify only one node is added
|
||||
assert mock_builder.add_node.call_count == 1
|
||||
|
||||
# Verify entry and finish points are the same node
|
||||
mock_builder.set_entry_point.assert_called_once_with("enhancer")
|
||||
mock_builder.set_finish_point.assert_called_once_with("enhancer")
|
||||
|
||||
@patch("src.prompt_enhancer.graph.builder.StateGraph")
|
||||
def test_build_graph_state_type(self, mock_state_graph):
|
||||
"""Test that the graph is initialized with the correct state type."""
|
||||
mock_builder = MagicMock()
|
||||
mock_compiled_graph = MagicMock()
|
||||
|
||||
mock_state_graph.return_value = mock_builder
|
||||
mock_builder.compile.return_value = mock_compiled_graph
|
||||
|
||||
build_graph()
|
||||
|
||||
# Verify StateGraph was initialized with PromptEnhancerState
|
||||
args, kwargs = mock_state_graph.call_args
|
||||
assert args[0] == PromptEnhancerState
|
||||
@@ -0,0 +1,219 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from langchain.schema import HumanMessage, SystemMessage
|
||||
|
||||
from src.prompt_enhancer.graph.enhancer_node import prompt_enhancer_node
|
||||
from src.prompt_enhancer.graph.state import PromptEnhancerState
|
||||
from src.config.report_style import ReportStyle
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_llm():
|
||||
"""Mock LLM that returns a test response."""
|
||||
llm = MagicMock()
|
||||
llm.invoke.return_value = MagicMock(content="Enhanced test prompt")
|
||||
return llm
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_messages():
|
||||
"""Mock messages returned by apply_prompt_template."""
|
||||
return [
|
||||
SystemMessage(content="System prompt template"),
|
||||
HumanMessage(content="Test human message"),
|
||||
]
|
||||
|
||||
|
||||
class TestPromptEnhancerNode:
|
||||
"""Test cases for prompt_enhancer_node function."""
|
||||
|
||||
@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_basic_prompt_enhancement(
|
||||
self, mock_get_llm, mock_apply_template, mock_llm, mock_messages
|
||||
):
|
||||
"""Test basic prompt enhancement without context or report style."""
|
||||
mock_get_llm.return_value = mock_llm
|
||||
mock_apply_template.return_value = mock_messages
|
||||
|
||||
state = PromptEnhancerState(prompt="Write about AI")
|
||||
|
||||
result = prompt_enhancer_node(state)
|
||||
|
||||
# Verify LLM was called
|
||||
mock_get_llm.assert_called_once_with("basic")
|
||||
mock_llm.invoke.assert_called_once_with(mock_messages)
|
||||
|
||||
# Verify apply_prompt_template was called correctly
|
||||
mock_apply_template.assert_called_once()
|
||||
call_args = mock_apply_template.call_args
|
||||
assert call_args[0][0] == "prompt_enhancer/prompt_enhancer"
|
||||
assert "messages" in call_args[0][1]
|
||||
assert "report_style" in call_args[0][1]
|
||||
|
||||
# Verify result
|
||||
assert result == {"output": "Enhanced test 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_prompt_enhancement_with_report_style(
|
||||
self, mock_get_llm, mock_apply_template, mock_llm, mock_messages
|
||||
):
|
||||
"""Test prompt enhancement with report style."""
|
||||
mock_get_llm.return_value = mock_llm
|
||||
mock_apply_template.return_value = mock_messages
|
||||
|
||||
state = PromptEnhancerState(
|
||||
prompt="Write about AI", report_style=ReportStyle.ACADEMIC
|
||||
)
|
||||
|
||||
result = prompt_enhancer_node(state)
|
||||
|
||||
# Verify apply_prompt_template was called with report_style
|
||||
mock_apply_template.assert_called_once()
|
||||
call_args = mock_apply_template.call_args
|
||||
assert call_args[0][0] == "prompt_enhancer/prompt_enhancer"
|
||||
assert call_args[0][1]["report_style"] == ReportStyle.ACADEMIC
|
||||
|
||||
# Verify result
|
||||
assert result == {"output": "Enhanced test 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_prompt_enhancement_with_context(
|
||||
self, mock_get_llm, mock_apply_template, mock_llm, mock_messages
|
||||
):
|
||||
"""Test prompt enhancement with additional context."""
|
||||
mock_get_llm.return_value = mock_llm
|
||||
mock_apply_template.return_value = mock_messages
|
||||
|
||||
state = PromptEnhancerState(
|
||||
prompt="Write about AI", context="Focus on machine learning applications"
|
||||
)
|
||||
|
||||
result = prompt_enhancer_node(state)
|
||||
|
||||
# Verify apply_prompt_template was called
|
||||
mock_apply_template.assert_called_once()
|
||||
call_args = mock_apply_template.call_args
|
||||
|
||||
# Check that the context was included in the human message
|
||||
messages_arg = call_args[0][1]["messages"]
|
||||
assert len(messages_arg) == 1
|
||||
human_message = messages_arg[0]
|
||||
assert isinstance(human_message, HumanMessage)
|
||||
assert "Focus on machine learning applications" in human_message.content
|
||||
|
||||
assert result == {"output": "Enhanced test 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_error_handling(
|
||||
self, mock_get_llm, mock_apply_template, mock_llm, mock_messages
|
||||
):
|
||||
"""Test error handling when LLM call fails."""
|
||||
mock_get_llm.return_value = mock_llm
|
||||
mock_apply_template.return_value = mock_messages
|
||||
|
||||
# Mock LLM to raise an exception
|
||||
mock_llm.invoke.side_effect = Exception("LLM error")
|
||||
|
||||
state = PromptEnhancerState(prompt="Test prompt")
|
||||
result = prompt_enhancer_node(state)
|
||||
|
||||
# Should return original prompt on error
|
||||
assert result == {"output": "Test 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_template_error_handling(
|
||||
self, mock_get_llm, mock_apply_template, mock_llm, mock_messages
|
||||
):
|
||||
"""Test error handling when template application fails."""
|
||||
mock_get_llm.return_value = mock_llm
|
||||
|
||||
# Mock apply_prompt_template to raise an exception
|
||||
mock_apply_template.side_effect = Exception("Template error")
|
||||
|
||||
state = PromptEnhancerState(prompt="Test prompt")
|
||||
result = prompt_enhancer_node(state)
|
||||
|
||||
# Should return original prompt on error
|
||||
assert result == {"output": "Test 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_prefix_removal(
|
||||
self, mock_get_llm, mock_apply_template, mock_llm, mock_messages
|
||||
):
|
||||
"""Test that common prefixes are removed from LLM response."""
|
||||
mock_get_llm.return_value = mock_llm
|
||||
mock_apply_template.return_value = mock_messages
|
||||
|
||||
# Test different prefixes that should be removed
|
||||
test_cases = [
|
||||
"Enhanced Prompt: This is the enhanced prompt",
|
||||
"Enhanced prompt: This is the enhanced prompt",
|
||||
"Here's the enhanced prompt: This is the enhanced prompt",
|
||||
"Here is the enhanced prompt: This is the enhanced prompt",
|
||||
"**Enhanced Prompt**: This is the enhanced prompt",
|
||||
"**Enhanced prompt**: This is the enhanced prompt",
|
||||
]
|
||||
|
||||
for response_with_prefix in test_cases:
|
||||
mock_llm.invoke.return_value = MagicMock(content=response_with_prefix)
|
||||
|
||||
state = PromptEnhancerState(prompt="Test prompt")
|
||||
result = prompt_enhancer_node(state)
|
||||
|
||||
assert result == {"output": "This is the 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_whitespace_handling(
|
||||
self, mock_get_llm, mock_apply_template, mock_llm, mock_messages
|
||||
):
|
||||
"""Test that whitespace is properly stripped from LLM response."""
|
||||
mock_get_llm.return_value = mock_llm
|
||||
mock_apply_template.return_value = mock_messages
|
||||
|
||||
# Mock LLM response with extra whitespace
|
||||
mock_llm.invoke.return_value = MagicMock(
|
||||
content=" \n\n Enhanced prompt \n\n "
|
||||
)
|
||||
|
||||
state = PromptEnhancerState(prompt="Test prompt")
|
||||
result = prompt_enhancer_node(state)
|
||||
|
||||
assert result == {"output": "Enhanced prompt"}
|
||||
@@ -0,0 +1,108 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import pytest
|
||||
from src.prompt_enhancer.graph.state import PromptEnhancerState
|
||||
from src.config.report_style import ReportStyle
|
||||
|
||||
|
||||
def test_prompt_enhancer_state_creation():
|
||||
"""Test that PromptEnhancerState can be created with required fields."""
|
||||
state = PromptEnhancerState(
|
||||
prompt="Test prompt", context=None, report_style=None, output=None
|
||||
)
|
||||
|
||||
assert state["prompt"] == "Test prompt"
|
||||
assert state["context"] is None
|
||||
assert state["report_style"] is None
|
||||
assert state["output"] is None
|
||||
|
||||
|
||||
def test_prompt_enhancer_state_with_all_fields():
|
||||
"""Test PromptEnhancerState with all fields populated."""
|
||||
state = PromptEnhancerState(
|
||||
prompt="Write about AI",
|
||||
context="Additional context about AI research",
|
||||
report_style=ReportStyle.ACADEMIC,
|
||||
output="Enhanced prompt about AI research",
|
||||
)
|
||||
|
||||
assert state["prompt"] == "Write about AI"
|
||||
assert state["context"] == "Additional context about AI research"
|
||||
assert state["report_style"] == ReportStyle.ACADEMIC
|
||||
assert state["output"] == "Enhanced prompt about AI research"
|
||||
|
||||
|
||||
def test_prompt_enhancer_state_minimal():
|
||||
"""Test PromptEnhancerState with only required prompt field."""
|
||||
state = PromptEnhancerState(prompt="Minimal prompt")
|
||||
|
||||
assert state["prompt"] == "Minimal prompt"
|
||||
# Optional fields should not be present if not specified
|
||||
assert "context" not in state
|
||||
assert "report_style" not in state
|
||||
assert "output" not in state
|
||||
|
||||
|
||||
def test_prompt_enhancer_state_with_different_report_styles():
|
||||
"""Test PromptEnhancerState with different ReportStyle values."""
|
||||
styles = [
|
||||
ReportStyle.ACADEMIC,
|
||||
ReportStyle.POPULAR_SCIENCE,
|
||||
ReportStyle.NEWS,
|
||||
ReportStyle.SOCIAL_MEDIA,
|
||||
]
|
||||
|
||||
for style in styles:
|
||||
state = PromptEnhancerState(prompt="Test prompt", report_style=style)
|
||||
assert state["report_style"] == style
|
||||
|
||||
|
||||
def test_prompt_enhancer_state_update():
|
||||
"""Test updating PromptEnhancerState fields."""
|
||||
state = PromptEnhancerState(prompt="Original prompt")
|
||||
|
||||
# Update with new fields
|
||||
state.update(
|
||||
{
|
||||
"context": "New context",
|
||||
"report_style": ReportStyle.NEWS,
|
||||
"output": "Enhanced output",
|
||||
}
|
||||
)
|
||||
|
||||
assert state["prompt"] == "Original prompt"
|
||||
assert state["context"] == "New context"
|
||||
assert state["report_style"] == ReportStyle.NEWS
|
||||
assert state["output"] == "Enhanced output"
|
||||
|
||||
|
||||
def test_prompt_enhancer_state_get_method():
|
||||
"""Test using get() method on PromptEnhancerState."""
|
||||
state = PromptEnhancerState(prompt="Test prompt", report_style=ReportStyle.ACADEMIC)
|
||||
|
||||
# Test get with existing keys
|
||||
assert state.get("prompt") == "Test prompt"
|
||||
assert state.get("report_style") == ReportStyle.ACADEMIC
|
||||
|
||||
# Test get with non-existing keys
|
||||
assert state.get("context") is None
|
||||
assert state.get("output") is None
|
||||
assert state.get("nonexistent", "default") == "default"
|
||||
|
||||
|
||||
def test_prompt_enhancer_state_type_annotations():
|
||||
"""Test that the state accepts correct types."""
|
||||
# This test ensures the TypedDict structure is working correctly
|
||||
state = PromptEnhancerState(
|
||||
prompt="Test prompt",
|
||||
context="Test context",
|
||||
report_style=ReportStyle.POPULAR_SCIENCE,
|
||||
output="Test output",
|
||||
)
|
||||
|
||||
# Verify types
|
||||
assert isinstance(state["prompt"], str)
|
||||
assert isinstance(state["context"], str)
|
||||
assert isinstance(state["report_style"], ReportStyle)
|
||||
assert isinstance(state["output"], str)
|
||||
@@ -0,0 +1,130 @@
|
||||
# 深度思考块功能实现总结
|
||||
|
||||
## 🎯 实现的功能
|
||||
|
||||
### 核心特性
|
||||
1. **智能展示逻辑**: 深度思考过程初始展开,计划内容开始时自动折叠
|
||||
2. **分阶段显示**: 思考阶段只显示思考块,思考结束后才显示计划卡片
|
||||
3. **动态主题**: 思考阶段使用蓝色主题,完成后切换为默认主题
|
||||
4. **流式支持**: 实时展示推理内容的流式传输
|
||||
5. **优雅交互**: 平滑的动画效果和状态切换
|
||||
|
||||
### 交互流程
|
||||
```
|
||||
用户发送问题 (启用深度思考)
|
||||
↓
|
||||
开始接收 reasoning_content
|
||||
↓
|
||||
思考块自动展开 + primary 主题 + 加载动画
|
||||
↓
|
||||
推理内容流式更新
|
||||
↓
|
||||
开始接收 content (计划内容)
|
||||
↓
|
||||
思考块自动折叠 + 主题切换
|
||||
↓
|
||||
计划卡片优雅出现 (动画效果)
|
||||
↓
|
||||
计划内容保持流式更新 (标题→思路→步骤)
|
||||
↓
|
||||
完成 (用户可手动展开思考块)
|
||||
```
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
### 数据结构扩展
|
||||
- `Message` 接口添加 `reasoningContent` 和 `reasoningContentChunks` 字段
|
||||
- `MessageChunkEvent` 接口添加 `reasoning_content` 字段
|
||||
- 消息合并逻辑支持推理内容的流式处理
|
||||
|
||||
### 组件架构
|
||||
- `ThoughtBlock`: 可折叠的思考块组件
|
||||
- `PlanCard`: 更新后的计划卡片,集成思考块
|
||||
- 智能状态管理和条件渲染
|
||||
|
||||
### 状态管理
|
||||
```typescript
|
||||
// 关键状态逻辑
|
||||
const hasMainContent = message.content && message.content.trim() !== "";
|
||||
const isThinking = reasoningContent && !hasMainContent;
|
||||
const shouldShowPlan = hasMainContent; // 有内容就显示,保持流式效果
|
||||
```
|
||||
|
||||
### 自动折叠逻辑
|
||||
```typescript
|
||||
React.useEffect(() => {
|
||||
if (hasMainContent && !hasAutoCollapsed) {
|
||||
setIsOpen(false);
|
||||
setHasAutoCollapsed(true);
|
||||
}
|
||||
}, [hasMainContent, hasAutoCollapsed]);
|
||||
```
|
||||
|
||||
## 🎨 视觉设计
|
||||
|
||||
### 统一设计语言
|
||||
- **字体系统**: 使用 `font-semibold` 与 CardTitle 保持一致
|
||||
- **圆角规范**: 采用 `rounded-xl` 与其他卡片组件统一
|
||||
- **间距标准**: 使用 `px-6 py-4` 内边距,`mb-6` 外边距
|
||||
- **图标尺寸**: 18px 大脑图标,与文字比例协调
|
||||
|
||||
### 思考阶段样式
|
||||
- Primary 主题色边框和背景
|
||||
- Primary 色图标和文字
|
||||
- 标准边框样式
|
||||
- 加载动画
|
||||
|
||||
### 完成阶段样式
|
||||
- 默认 border 和 card 背景
|
||||
- muted-foreground 图标
|
||||
- 80% 透明度文字
|
||||
- 静态图标
|
||||
|
||||
### 动画效果
|
||||
- 展开/折叠动画
|
||||
- 主题切换过渡
|
||||
- 颜色变化动画
|
||||
|
||||
## 📁 文件更改
|
||||
|
||||
### 核心文件
|
||||
1. `web/src/core/messages/types.ts` - 消息类型扩展
|
||||
2. `web/src/core/api/types.ts` - API 事件类型扩展
|
||||
3. `web/src/core/messages/merge-message.ts` - 消息合并逻辑
|
||||
4. `web/src/core/store/store.ts` - 状态管理更新
|
||||
5. `web/src/app/chat/components/message-list-view.tsx` - 主要组件实现
|
||||
|
||||
### 测试和文档
|
||||
1. `web/public/mock/reasoning-example.txt` - 测试数据
|
||||
2. `web/docs/thought-block-feature.md` - 功能文档
|
||||
3. `web/docs/testing-thought-block.md` - 测试指南
|
||||
4. `web/docs/interaction-flow-test.md` - 交互流程测试
|
||||
|
||||
## 🧪 测试方法
|
||||
|
||||
### 快速测试
|
||||
```
|
||||
访问: http://localhost:3000?mock=reasoning-example
|
||||
发送任意消息,观察交互流程
|
||||
```
|
||||
|
||||
### 完整测试
|
||||
1. 启用深度思考模式
|
||||
2. 配置 reasoning 模型
|
||||
3. 发送复杂问题
|
||||
4. 验证完整交互流程
|
||||
|
||||
## 🔄 兼容性
|
||||
|
||||
- ✅ 向后兼容:无推理内容时正常显示
|
||||
- ✅ 渐进增强:功能仅在有推理内容时激活
|
||||
- ✅ 优雅降级:推理内容为空时不显示思考块
|
||||
|
||||
## 🚀 使用建议
|
||||
|
||||
1. **启用深度思考**: 点击"Deep Thinking"按钮
|
||||
2. **观察流程**: 注意思考块的自动展开和折叠
|
||||
3. **手动控制**: 可随时点击思考块标题栏控制展开/折叠
|
||||
4. **查看推理**: 展开思考块查看完整的推理过程
|
||||
|
||||
这个实现完全满足了用户的需求,提供了直观、流畅的深度思考过程展示体验。
|
||||
@@ -0,0 +1,112 @@
|
||||
# 思考块交互流程测试
|
||||
|
||||
## 测试场景
|
||||
|
||||
### 场景 1: 完整的深度思考流程
|
||||
|
||||
**步骤**:
|
||||
1. 启用深度思考模式
|
||||
2. 发送问题:"什么是 vibe coding?"
|
||||
3. 观察交互流程
|
||||
|
||||
**预期行为**:
|
||||
|
||||
#### 阶段 1: 深度思考开始
|
||||
- ✅ 思考块立即出现并展开
|
||||
- ✅ 使用蓝色主题(边框、背景、图标、文字)
|
||||
- ✅ 显示加载动画
|
||||
- ✅ 不显示计划卡片
|
||||
- ✅ 推理内容实时流式更新
|
||||
|
||||
#### 阶段 2: 思考过程中
|
||||
- ✅ 思考块保持展开状态
|
||||
- ✅ 蓝色主题持续显示
|
||||
- ✅ 推理内容持续增加
|
||||
- ✅ 加载动画持续显示
|
||||
- ✅ 计划卡片仍然不显示
|
||||
|
||||
#### 阶段 3: 开始接收计划内容
|
||||
- ✅ 思考块自动折叠
|
||||
- ✅ 主题从 primary 切换为默认
|
||||
- ✅ 加载动画消失
|
||||
- ✅ 计划卡片以优雅动画出现(opacity: 0→1, y: 20→0)
|
||||
- ✅ 计划内容保持流式更新效果
|
||||
|
||||
#### 阶段 4: 计划流式输出
|
||||
- ✅ 标题逐步显示
|
||||
- ✅ 思路内容流式更新
|
||||
- ✅ 步骤列表逐项显示
|
||||
- ✅ 每个步骤的标题和描述分别流式渲染
|
||||
|
||||
#### 阶段 5: 计划完成
|
||||
- ✅ 思考块保持折叠状态
|
||||
- ✅ 计划卡片完全显示
|
||||
- ✅ 用户可手动展开思考块查看推理过程
|
||||
|
||||
### 场景 2: 手动交互测试
|
||||
|
||||
**步骤**:
|
||||
1. 在思考完成后,手动点击思考块
|
||||
2. 验证展开/折叠功能
|
||||
|
||||
**预期行为**:
|
||||
- ✅ 点击可正常展开/折叠
|
||||
- ✅ 动画效果流畅
|
||||
- ✅ 内容完整显示
|
||||
- ✅ 不影响计划卡片显示
|
||||
|
||||
### 场景 3: 边界情况测试
|
||||
|
||||
#### 3.1 只有推理内容,没有计划内容
|
||||
**预期**: 思考块保持展开,不显示计划卡片
|
||||
|
||||
#### 3.2 没有推理内容,只有计划内容
|
||||
**预期**: 不显示思考块,直接显示计划卡片
|
||||
|
||||
#### 3.3 推理内容为空
|
||||
**预期**: 不显示思考块,直接显示计划卡片
|
||||
|
||||
## 验证要点
|
||||
|
||||
### 视觉效果
|
||||
- [ ] Primary 主题色在思考阶段正确显示
|
||||
- [ ] 主题切换动画流畅
|
||||
- [ ] 字体权重与 CardTitle 保持一致 (`font-semibold`)
|
||||
- [ ] 圆角设计与其他卡片统一 (`rounded-xl`)
|
||||
- [ ] 图标尺寸和颜色正确变化 (18px, primary/muted-foreground)
|
||||
- [ ] 内边距与设计系统一致 (`px-6 py-4`)
|
||||
- [ ] 整体视觉层次与页面协调
|
||||
|
||||
### 交互逻辑
|
||||
- [ ] 自动展开/折叠时机正确
|
||||
- [ ] 手动展开/折叠功能正常
|
||||
- [ ] 计划卡片显示时机正确
|
||||
- [ ] 加载动画显示时机正确
|
||||
|
||||
### 内容渲染
|
||||
- [ ] 推理内容正确流式更新
|
||||
- [ ] Markdown 格式正确渲染
|
||||
- [ ] 中文内容正确显示
|
||||
- [ ] 内容不丢失或重复
|
||||
|
||||
### 性能表现
|
||||
- [ ] 动画流畅,无卡顿
|
||||
- [ ] 内存使用正常
|
||||
- [ ] 组件重新渲染次数合理
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 思考块不自动折叠
|
||||
1. 检查 `hasMainContent` 逻辑
|
||||
2. 验证 `useEffect` 依赖项
|
||||
3. 确认 `hasAutoCollapsed` 状态管理
|
||||
|
||||
### 计划卡片显示时机错误
|
||||
1. 检查 `shouldShowPlan` 计算逻辑
|
||||
2. 验证 `isThinking` 状态判断
|
||||
3. 确认消息内容解析正确
|
||||
|
||||
### 主题切换异常
|
||||
1. 检查 `isStreaming` 状态
|
||||
2. 验证 CSS 类名应用
|
||||
3. 确认条件渲染逻辑
|
||||
@@ -0,0 +1,125 @@
|
||||
# 流式输出优化改进
|
||||
|
||||
## 🎯 改进目标
|
||||
|
||||
确保在深度思考结束后,plan block 保持流式输出效果,提供更流畅丝滑的用户体验。
|
||||
|
||||
## 🔧 技术改进
|
||||
|
||||
### 状态逻辑优化
|
||||
|
||||
**之前的逻辑**:
|
||||
```typescript
|
||||
const isThinking = reasoningContent && (!hasMainContent || message.isStreaming);
|
||||
const shouldShowPlan = hasMainContent && !isThinking;
|
||||
```
|
||||
|
||||
**优化后的逻辑**:
|
||||
```typescript
|
||||
const isThinking = reasoningContent && !hasMainContent;
|
||||
const shouldShowPlan = hasMainContent; // 简化逻辑,有内容就显示
|
||||
```
|
||||
|
||||
### 关键改进点
|
||||
|
||||
1. **简化显示逻辑**: 只要有主要内容就显示 plan,不再依赖思考状态
|
||||
2. **保持流式状态**: plan 组件的 `animated` 属性直接使用 `message.isStreaming`
|
||||
3. **优雅入场动画**: 添加 motion.div 包装,提供平滑的出现效果
|
||||
|
||||
## 🎨 用户体验提升
|
||||
|
||||
### 流式输出效果
|
||||
|
||||
#### 思考阶段
|
||||
- ✅ 推理内容实时流式更新
|
||||
- ✅ 思考块保持展开状态
|
||||
- ✅ Primary 主题色高亮显示
|
||||
|
||||
#### 计划阶段
|
||||
- ✅ 计划卡片优雅出现(300ms 动画)
|
||||
- ✅ 标题内容流式渲染
|
||||
- ✅ 思路内容流式更新
|
||||
- ✅ 步骤列表逐项显示
|
||||
- ✅ 每个步骤的标题和描述分别流式渲染
|
||||
|
||||
### 动画效果
|
||||
|
||||
#### 计划卡片入场动画
|
||||
```typescript
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
```
|
||||
|
||||
#### 流式文本动画
|
||||
- 所有 Markdown 组件都使用 `animated={message.isStreaming}`
|
||||
- 确保文本逐字符或逐词显示效果
|
||||
|
||||
## 📊 性能优化
|
||||
|
||||
### 渲染优化
|
||||
- **减少重新渲染**: 简化状态逻辑,减少不必要的组件重新挂载
|
||||
- **保持组件实例**: plan 组件一旦出现就保持存在,避免重新创建
|
||||
- **流式状态传递**: 直接使用消息的流式状态,避免额外的状态计算
|
||||
|
||||
### 内存优化
|
||||
- **组件复用**: 避免频繁的组件销毁和重建
|
||||
- **状态管理**: 简化状态依赖,减少内存占用
|
||||
|
||||
## 🧪 测试验证
|
||||
|
||||
### 流式效果验证
|
||||
1. **思考阶段**: 推理内容应该逐步显示
|
||||
2. **过渡阶段**: 计划卡片应该平滑出现
|
||||
3. **计划阶段**: 所有计划内容应该保持流式效果
|
||||
|
||||
### 动画效果验证
|
||||
1. **入场动画**: 计划卡片应该从下方滑入并淡入
|
||||
2. **文本动画**: 所有文本内容应该有打字机效果
|
||||
3. **状态切换**: 思考块折叠应该平滑自然
|
||||
|
||||
### 性能验证
|
||||
1. **渲染次数**: 检查组件重新渲染频率
|
||||
2. **内存使用**: 监控内存占用情况
|
||||
3. **动画流畅度**: 确保 60fps 的动画效果
|
||||
|
||||
## 📝 使用示例
|
||||
|
||||
### 完整交互流程
|
||||
```
|
||||
1. 用户发送问题 (启用深度思考)
|
||||
↓
|
||||
2. 思考块展开,推理内容流式显示
|
||||
↓
|
||||
3. 开始接收计划内容
|
||||
↓
|
||||
4. 思考块自动折叠
|
||||
↓
|
||||
5. 计划卡片优雅出现 (动画效果)
|
||||
↓
|
||||
6. 计划内容流式渲染:
|
||||
- 标题逐步显示
|
||||
- 思路内容流式更新
|
||||
- 步骤列表逐项显示
|
||||
↓
|
||||
7. 完成,用户可查看完整内容
|
||||
```
|
||||
|
||||
## 🔄 兼容性
|
||||
|
||||
- ✅ **向后兼容**: 不影响现有的非深度思考模式
|
||||
- ✅ **渐进增强**: 功能仅在有推理内容时激活
|
||||
- ✅ **优雅降级**: 在不支持的环境中正常显示
|
||||
|
||||
## 🚀 效果总结
|
||||
|
||||
这次优化显著提升了用户体验:
|
||||
|
||||
1. **更流畅的过渡**: 从思考到计划的切换更加自然
|
||||
2. **保持流式效果**: 计划内容保持了原有的流式输出特性
|
||||
3. **视觉连贯性**: 整个过程的视觉效果更加连贯统一
|
||||
4. **性能提升**: 减少了不必要的组件重新渲染
|
||||
|
||||
用户现在可以享受到完整的流式体验,从深度思考到计划展示都保持了一致的流畅感。
|
||||
@@ -0,0 +1,78 @@
|
||||
# 测试思考块功能
|
||||
|
||||
## 快速测试
|
||||
|
||||
### 方法 1: 使用模拟数据
|
||||
|
||||
1. 在浏览器中访问应用并添加 `?mock=reasoning-example` 参数
|
||||
2. 发送任意消息
|
||||
3. 观察计划卡片上方是否出现思考块
|
||||
|
||||
### 方法 2: 启用深度思考模式
|
||||
|
||||
1. 确保配置了 reasoning 模型(如 DeepSeek R1)
|
||||
2. 在聊天界面点击"Deep Thinking"按钮
|
||||
3. 发送一个需要规划的问题
|
||||
4. 观察是否出现思考块
|
||||
|
||||
## 预期行为
|
||||
|
||||
### 思考块外观
|
||||
- 深度思考开始时自动展开显示
|
||||
- 思考阶段使用 primary 主题色(边框、背景、文字、图标)
|
||||
- 带有 18px 大脑图标和"深度思考过程"标题
|
||||
- 使用 `font-semibold` 字体权重,与 CardTitle 保持一致
|
||||
- `rounded-xl` 圆角设计,与其他卡片组件统一
|
||||
- 标准的 `px-6 py-4` 内边距
|
||||
|
||||
### 交互行为
|
||||
- 思考阶段:自动展开,蓝色高亮,显示加载动画
|
||||
- 计划阶段:自动折叠,切换为默认主题
|
||||
- 用户可随时手动展开/折叠
|
||||
- 平滑的展开/折叠动画和主题切换
|
||||
|
||||
### 分阶段显示
|
||||
- 思考阶段:只显示思考块,不显示计划卡片
|
||||
- 计划阶段:思考块折叠,显示完整计划卡片
|
||||
|
||||
### 内容渲染
|
||||
- 支持 Markdown 格式
|
||||
- 中文内容正确显示
|
||||
- 保持原有的换行和格式
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 思考块不显示
|
||||
1. 检查消息是否包含 `reasoningContent` 字段
|
||||
2. 确认 `reasoning_content` 事件是否正确处理
|
||||
3. 验证消息合并逻辑是否正常工作
|
||||
|
||||
### 内容显示异常
|
||||
1. 检查 Markdown 渲染是否正常
|
||||
2. 确认 CSS 样式是否正确加载
|
||||
3. 验证动画效果是否启用
|
||||
|
||||
### 流式传输问题
|
||||
1. 检查 WebSocket 连接状态
|
||||
2. 确认事件流格式是否正确
|
||||
3. 验证消息更新逻辑
|
||||
|
||||
## 开发调试
|
||||
|
||||
### 控制台检查
|
||||
```javascript
|
||||
// 检查消息对象
|
||||
const messages = useStore.getState().messages;
|
||||
const lastMessage = Array.from(messages.values()).pop();
|
||||
console.log('Reasoning content:', lastMessage?.reasoningContent);
|
||||
```
|
||||
|
||||
### 网络面板
|
||||
- 查看 SSE 事件流
|
||||
- 确认 `reasoning_content` 字段存在
|
||||
- 检查事件格式是否正确
|
||||
|
||||
### React DevTools
|
||||
- 检查 ThoughtBlock 组件状态
|
||||
- 验证 props 传递是否正确
|
||||
- 观察组件重新渲染情况
|
||||
@@ -0,0 +1,155 @@
|
||||
# 思考块设计系统规范
|
||||
|
||||
## 🎯 设计目标
|
||||
|
||||
确保思考块组件与整个应用的设计语言保持完全一致,提供统一的用户体验。
|
||||
|
||||
## 📐 设计规范
|
||||
|
||||
### 字体系统
|
||||
```css
|
||||
/* 标题字体 - 与 CardTitle 保持一致 */
|
||||
font-weight: 600; /* font-semibold */
|
||||
line-height: 1; /* leading-none */
|
||||
```
|
||||
|
||||
### 尺寸规范
|
||||
```css
|
||||
/* 图标尺寸 */
|
||||
icon-size: 18px; /* 与文字比例协调 */
|
||||
|
||||
/* 内边距 */
|
||||
padding: 1.5rem; /* px-6 py-4 */
|
||||
|
||||
/* 外边距 */
|
||||
margin-bottom: 1.5rem; /* mb-6 */
|
||||
|
||||
/* 圆角 */
|
||||
border-radius: 0.75rem; /* rounded-xl */
|
||||
```
|
||||
|
||||
### 颜色系统
|
||||
|
||||
#### 思考阶段(活跃状态)
|
||||
```css
|
||||
/* 边框和背景 */
|
||||
border-color: hsl(var(--primary) / 0.2);
|
||||
background-color: hsl(var(--primary) / 0.05);
|
||||
|
||||
/* 图标和文字 */
|
||||
color: hsl(var(--primary));
|
||||
|
||||
/* 阴影 */
|
||||
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
```
|
||||
|
||||
#### 完成阶段(静态状态)
|
||||
```css
|
||||
/* 边框和背景 */
|
||||
border-color: hsl(var(--border));
|
||||
background-color: hsl(var(--card));
|
||||
|
||||
/* 图标 */
|
||||
color: hsl(var(--muted-foreground));
|
||||
|
||||
/* 文字 */
|
||||
color: hsl(var(--foreground));
|
||||
```
|
||||
|
||||
#### 内容区域
|
||||
```css
|
||||
/* 思考阶段 */
|
||||
.prose-primary {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
/* 完成阶段 */
|
||||
.opacity-80 {
|
||||
opacity: 0.8;
|
||||
}
|
||||
```
|
||||
|
||||
### 交互状态
|
||||
```css
|
||||
/* 悬停状态 */
|
||||
.hover\:bg-accent:hover {
|
||||
background-color: hsl(var(--accent));
|
||||
}
|
||||
|
||||
.hover\:text-accent-foreground:hover {
|
||||
color: hsl(var(--accent-foreground));
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 状态变化
|
||||
|
||||
### 状态映射
|
||||
| 状态 | 边框 | 背景 | 图标颜色 | 文字颜色 | 阴影 |
|
||||
|------|------|------|----------|----------|------|
|
||||
| 思考中 | primary/20 | primary/5 | primary | primary | 有 |
|
||||
| 已完成 | border | card | muted-foreground | foreground | 无 |
|
||||
|
||||
### 动画过渡
|
||||
```css
|
||||
transition: all 200ms ease-in-out;
|
||||
```
|
||||
|
||||
## 📱 响应式设计
|
||||
|
||||
### 间距适配
|
||||
- 移动端:保持相同的内边距比例
|
||||
- 桌面端:标准的 `px-6 py-4` 内边距
|
||||
|
||||
### 字体适配
|
||||
- 所有设备:保持 `font-semibold` 字体权重
|
||||
- 图标尺寸:固定 18px,确保清晰度
|
||||
|
||||
## 🎨 与现有组件的对比
|
||||
|
||||
### CardTitle 对比
|
||||
| 属性 | CardTitle | ThoughtBlock |
|
||||
|------|-----------|--------------|
|
||||
| 字体权重 | font-semibold | font-semibold ✅ |
|
||||
| 行高 | leading-none | leading-none ✅ |
|
||||
| 颜色 | foreground | primary/foreground |
|
||||
|
||||
### Card 对比
|
||||
| 属性 | Card | ThoughtBlock |
|
||||
|------|------|--------------|
|
||||
| 圆角 | rounded-lg | rounded-xl |
|
||||
| 边框 | border | border ✅ |
|
||||
| 背景 | card | card/primary ✅ |
|
||||
|
||||
### Button 对比
|
||||
| 属性 | Button | ThoughtBlock Trigger |
|
||||
|------|--------|---------------------|
|
||||
| 内边距 | 标准 | px-6 py-4 ✅ |
|
||||
| 悬停 | hover:bg-accent | hover:bg-accent ✅ |
|
||||
| 圆角 | rounded-md | rounded-xl |
|
||||
|
||||
## ✅ 设计检查清单
|
||||
|
||||
### 视觉一致性
|
||||
- [ ] 字体权重与 CardTitle 一致
|
||||
- [ ] 圆角设计与卡片组件统一
|
||||
- [ ] 颜色使用 CSS 变量系统
|
||||
- [ ] 间距符合设计规范
|
||||
|
||||
### 交互一致性
|
||||
- [ ] 悬停状态与 Button 组件一致
|
||||
- [ ] 过渡动画时长统一(200ms)
|
||||
- [ ] 状态变化平滑自然
|
||||
|
||||
### 可访问性
|
||||
- [ ] 颜色对比度符合 WCAG 标准
|
||||
- [ ] 图标尺寸适合点击/触摸
|
||||
- [ ] 状态变化有明确的视觉反馈
|
||||
|
||||
## 🔧 实现要点
|
||||
|
||||
1. **使用设计系统变量**: 所有颜色都使用 CSS 变量,确保主题切换正常
|
||||
2. **保持组件一致性**: 与现有 Card、Button 组件的样式保持一致
|
||||
3. **响应式友好**: 在不同设备上都有良好的显示效果
|
||||
4. **性能优化**: 使用 CSS 过渡而非 JavaScript 动画
|
||||
|
||||
这个设计系统确保了思考块组件与整个应用的视觉语言完全统一,提供了一致的用户体验。
|
||||
@@ -0,0 +1,108 @@
|
||||
# 思考块功能 (Thought Block Feature)
|
||||
|
||||
## 概述
|
||||
|
||||
思考块功能允许在计划卡片之前展示 AI 的深度思考过程,以可折叠的方式呈现推理内容。这个功能特别适用于启用深度思考模式时的场景。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **智能展示逻辑**: 深度思考过程初始展开,当开始接收计划内容时自动折叠
|
||||
- **分阶段显示**: 思考阶段只显示思考块,思考结束后才显示计划卡片
|
||||
- **流式支持**: 支持推理内容的实时流式展示
|
||||
- **视觉状态反馈**: 思考阶段使用蓝色主题突出显示
|
||||
- **优雅的动画**: 包含平滑的展开/折叠动画效果
|
||||
- **响应式设计**: 适配不同屏幕尺寸
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 数据结构更新
|
||||
|
||||
1. **Message 类型扩展**:
|
||||
```typescript
|
||||
export interface Message {
|
||||
// ... 其他字段
|
||||
reasoningContent?: string;
|
||||
reasoningContentChunks?: string[];
|
||||
}
|
||||
```
|
||||
|
||||
2. **API 事件类型扩展**:
|
||||
```typescript
|
||||
export interface MessageChunkEvent {
|
||||
// ... 其他字段
|
||||
reasoning_content?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 组件结构
|
||||
|
||||
- **ThoughtBlock**: 主要的思考块组件
|
||||
- 使用 Radix UI 的 Collapsible 组件
|
||||
- 支持流式内容展示
|
||||
- 包含加载动画和状态指示
|
||||
|
||||
- **PlanCard**: 更新后的计划卡片
|
||||
- 在计划内容之前展示思考块
|
||||
- 自动检测是否有推理内容
|
||||
|
||||
### 消息处理
|
||||
|
||||
消息合并逻辑已更新以支持 `reasoning_content` 字段的流式处理:
|
||||
|
||||
```typescript
|
||||
function mergeTextMessage(message: Message, event: MessageChunkEvent) {
|
||||
// 处理常规内容
|
||||
if (event.data.content) {
|
||||
message.content += event.data.content;
|
||||
message.contentChunks.push(event.data.content);
|
||||
}
|
||||
|
||||
// 处理推理内容
|
||||
if (event.data.reasoning_content) {
|
||||
message.reasoningContent = (message.reasoningContent || "") + event.data.reasoning_content;
|
||||
message.reasoningContentChunks = message.reasoningContentChunks || [];
|
||||
message.reasoningContentChunks.push(event.data.reasoning_content);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 启用深度思考模式
|
||||
|
||||
1. 在聊天界面中,点击"Deep Thinking"按钮
|
||||
2. 确保配置了支持推理的模型
|
||||
3. 发送消息后,如果有推理内容,会在计划卡片上方显示思考块
|
||||
|
||||
### 查看推理过程
|
||||
|
||||
1. 深度思考开始时,思考块自动展开显示
|
||||
2. 思考阶段使用 primary 主题色,突出显示正在进行的推理过程
|
||||
3. 推理内容支持 Markdown 格式渲染,实时流式更新
|
||||
4. 在流式传输过程中会显示加载动画
|
||||
5. 当开始接收计划内容时,思考块自动折叠
|
||||
6. 计划卡片以优雅的动画效果出现
|
||||
7. 计划内容保持流式输出效果,逐步显示标题、思路和步骤
|
||||
8. 用户可以随时点击思考块标题栏手动展开/折叠
|
||||
|
||||
## 样式特性
|
||||
|
||||
- **统一设计语言**: 与页面整体设计风格保持一致
|
||||
- **字体层次**: 使用与 CardTitle 相同的 `font-semibold` 字体权重
|
||||
- **圆角设计**: 采用 `rounded-xl` 与其他卡片组件保持一致
|
||||
- **间距规范**: 使用标准的 `px-6 py-4` 内边距
|
||||
- **动态主题**: 思考阶段使用 primary 色彩系统
|
||||
- **图标尺寸**: 18px 图标尺寸,与文字比例协调
|
||||
- **状态反馈**: 流式传输时显示加载动画和主题色高亮
|
||||
- **交互反馈**: 标准的 hover 和 focus 状态
|
||||
- **平滑过渡**: 所有状态变化都有平滑的过渡动画
|
||||
|
||||
## 测试数据
|
||||
|
||||
可以使用 `/mock/reasoning-example.txt` 文件测试思考块功能,该文件包含了模拟的推理内容和计划数据。
|
||||
|
||||
## 兼容性
|
||||
|
||||
- 向后兼容:没有推理内容的消息不会显示思考块
|
||||
- 渐进增强:功能仅在有推理内容时激活
|
||||
- 优雅降级:如果推理内容为空,组件不会渲染
|
||||
+8
-2
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"check": "next lint && tsc --noEmit",
|
||||
"dev": "next dev --turbo",
|
||||
"dev": "dotenv -e ../.env -- next dev --turbo",
|
||||
"scan": "next dev & npx react-scan@latest localhost:3000",
|
||||
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
||||
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
||||
@@ -35,12 +35,16 @@
|
||||
"@radix-ui/react-switch": "^1.2.2",
|
||||
"@radix-ui/react-tabs": "^1.1.4",
|
||||
"@radix-ui/react-tooltip": "^1.2.0",
|
||||
"@rc-component/mentions": "^1.2.0",
|
||||
"@t3-oss/env-nextjs": "^0.11.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tiptap/extension-document": "^2.12.0",
|
||||
"@tiptap/extension-mention": "^2.12.0",
|
||||
"@tiptap/extension-table": "^2.11.7",
|
||||
"@tiptap/extension-table-cell": "^2.11.7",
|
||||
"@tiptap/extension-table-header": "^2.11.7",
|
||||
"@tiptap/extension-table-row": "^2.11.7",
|
||||
"@tiptap/extension-text": "^2.12.0",
|
||||
"@tiptap/react": "^2.11.7",
|
||||
"@xyflow/react": "^12.6.0",
|
||||
"best-effort-json-parser": "^1.1.3",
|
||||
@@ -70,6 +74,7 @@
|
||||
"remark-math": "^6.0.0",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"tw-animate-css": "^1.2.5",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
@@ -86,6 +91,7 @@
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-config-next": "^15.2.3",
|
||||
"postcss": "^8.5.3",
|
||||
@@ -105,4 +111,4 @@
|
||||
"sharp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+228
-8
@@ -62,12 +62,21 @@ importers:
|
||||
'@radix-ui/react-tooltip':
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0(@types/react-dom@19.1.1(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/mentions':
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@t3-oss/env-nextjs':
|
||||
specifier: ^0.11.0
|
||||
version: 0.11.1(typescript@5.8.3)(zod@3.24.3)
|
||||
'@tailwindcss/typography':
|
||||
specifier: ^0.5.16
|
||||
version: 0.5.16(tailwindcss@4.1.4)
|
||||
'@tiptap/extension-document':
|
||||
specifier: ^2.12.0
|
||||
version: 2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-mention':
|
||||
specifier: ^2.12.0
|
||||
version: 2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(@tiptap/suggestion@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-table':
|
||||
specifier: ^2.11.7
|
||||
version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
|
||||
@@ -80,6 +89,9 @@ importers:
|
||||
'@tiptap/extension-table-row':
|
||||
specifier: ^2.11.7
|
||||
version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-text':
|
||||
specifier: ^2.12.0
|
||||
version: 2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/react':
|
||||
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)
|
||||
@@ -167,6 +179,9 @@ importers:
|
||||
tailwind-merge:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
tippy.js:
|
||||
specifier: ^6.3.7
|
||||
version: 6.3.7
|
||||
tiptap-markdown:
|
||||
specifier: ^0.8.10
|
||||
version: 0.8.10(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
@@ -210,6 +225,9 @@ importers:
|
||||
'@types/react-syntax-highlighter':
|
||||
specifier: ^15.5.13
|
||||
version: 15.5.13
|
||||
dotenv-cli:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0
|
||||
eslint:
|
||||
specifier: ^9.23.0
|
||||
version: 9.24.0(jiti@2.4.2)
|
||||
@@ -1193,6 +1211,56 @@ packages:
|
||||
'@radix-ui/rect@1.1.1':
|
||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||
|
||||
'@rc-component/input@1.0.1':
|
||||
resolution: {integrity: sha512-omxsjWpB+RamzDDB0NzgV6qI7Ok/U6nrN2KLL/hLZJcI7sZZgLYAN+Xs1pN7OYBnUeyn25PizcntEE0nofHv8Q==}
|
||||
peerDependencies:
|
||||
react: '>=16.0.0'
|
||||
react-dom: '>=16.0.0'
|
||||
|
||||
'@rc-component/mentions@1.2.0':
|
||||
resolution: {integrity: sha512-dSr9mX5bQWDegeVLr+NoffjZO5paG/nzM5f+RVslpznfVqR5d3c+xan+f6ZqZWHJqJOfROqNGAkUb8pqqAV7wQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
'@rc-component/menu@1.1.3':
|
||||
resolution: {integrity: sha512-NN/J0nJFwwDfQBycl9mordDTBdSai5Ie4nxaGkH2eHVa37KjyhpU98EtcVb/ss393I7SZTDCvoylS3MQOjgYkw==}
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
'@rc-component/motion@1.1.4':
|
||||
resolution: {integrity: sha512-rz3+kqQ05xEgIAB9/UKQZKCg5CO/ivGNU78QWYKVfptmbjJKynZO4KXJ7pJD3oMxE9aW94LD/N3eppXWeysTjw==}
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
'@rc-component/portal@2.0.0':
|
||||
resolution: {integrity: sha512-337ADhBfgH02S8OujUl33OT+8zVJ67eyuUq11j/dE71rXKYNihMsggW8R2VfI2aL3SciDp8gAFsmPVoPkxLUGw==}
|
||||
engines: {node: '>=12.x'}
|
||||
peerDependencies:
|
||||
react: '>=18.0.0'
|
||||
react-dom: '>=18.0.0'
|
||||
|
||||
'@rc-component/resize-observer@1.0.0':
|
||||
resolution: {integrity: sha512-inR8Ka87OOwtrDJzdVp2VuEVlc5nK20lHolvkwFUnXwV50p+nLhKny1NvNTCKvBmS/pi/rTn/1Hvsw10sRRnXA==}
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
'@rc-component/textarea@1.0.0':
|
||||
resolution: {integrity: sha512-GuXakeRWZuWUnF2sqfC8RjtzfAh5UI89dPk6r5SgosyQGfQIueuN8LkWmFq5OKTOJIlc82MOjHiPBigKB9+KGw==}
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
'@rc-component/trigger@3.4.0':
|
||||
resolution: {integrity: sha512-Vu+RS7bGAHHNtzP6EzrMwH+xiZl+SHQgR98oAUXtoQIy4+4lsSppwQPcl6Q7ORZuZevil1BSw4GHXNWD8BJOXw==}
|
||||
engines: {node: '>=8.x'}
|
||||
peerDependencies:
|
||||
react: '>=18.0.0'
|
||||
react-dom: '>=18.0.0'
|
||||
|
||||
'@rc-component/util@1.2.1':
|
||||
resolution: {integrity: sha512-AUVu6jO+lWjQnUOOECwu8iR0EdElQgWW5NBv5vP/Uf9dWbAX3udhMutRlkVXjuac2E40ghkFy+ve00mc/3Fymg==}
|
||||
peerDependencies:
|
||||
@@ -1399,8 +1467,8 @@ packages:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/extension-text-style': ^2.7.0
|
||||
|
||||
'@tiptap/extension-document@2.11.7':
|
||||
resolution: {integrity: sha512-95ouJXPjdAm9+VBRgFo4lhDoMcHovyl/awORDI8gyEn0Rdglt+ZRZYoySFzbVzer9h0cre+QdIwr9AIzFFbfdA==}
|
||||
'@tiptap/extension-document@2.12.0':
|
||||
resolution: {integrity: sha512-sA1Q+mxDIv0Y3qQTBkYGwknNbDcGFiJ/fyAFholXpqbrcRx3GavwR/o0chBdsJZlFht0x7AWGwUYWvIo7wYilA==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
@@ -1470,6 +1538,13 @@ packages:
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
'@tiptap/extension-mention@2.12.0':
|
||||
resolution: {integrity: sha512-+b/fqOU+pRWWAo0ZfyInkhkvV0Ub5RpNrYZ45v2nn5PjbXbxyxNQ51zT6cGk2F6Jmc6UBmlR8iqqNTIQY9ieEg==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/pm': ^2.7.0
|
||||
'@tiptap/suggestion': ^2.7.0
|
||||
|
||||
'@tiptap/extension-ordered-list@2.11.7':
|
||||
resolution: {integrity: sha512-bLGCHDMB0vbJk7uu8bRg8vES3GsvxkX7Cgjgm/6xysHFbK98y0asDtNxkW1VvuRreNGz4tyB6vkcVCfrxl4jKw==}
|
||||
peerDependencies:
|
||||
@@ -1528,8 +1603,8 @@ packages:
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
'@tiptap/extension-text@2.11.7':
|
||||
resolution: {integrity: sha512-wObCn8qZkIFnXTLvBP+X8KgaEvTap/FJ/i4hBMfHBCKPGDx99KiJU6VIbDXG8d5ZcFZE0tOetK1pP5oI7qgMlQ==}
|
||||
'@tiptap/extension-text@2.12.0':
|
||||
resolution: {integrity: sha512-0ytN9V1tZYTXdiYDQg4FB2SQ56JAJC9r/65snefb9ztl+gZzDrIvih7CflHs1ic9PgyjexfMLeH+VzuMccNyZw==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
@@ -2221,6 +2296,18 @@ packages:
|
||||
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
dotenv-cli@8.0.0:
|
||||
resolution: {integrity: sha512-aLqYbK7xKOiTMIRf1lDPbI+Y+Ip/wo5k3eyp6ePysVaSqbyxjyK3dK35BTxG+rmd7djf5q2UPs4noPNH+cj0Qw==}
|
||||
hasBin: true
|
||||
|
||||
dotenv-expand@10.0.0:
|
||||
resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dotenv@16.5.0:
|
||||
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3520,6 +3607,24 @@ packages:
|
||||
peerDependencies:
|
||||
webpack: ^4.0.0 || ^5.0.0
|
||||
|
||||
rc-overflow@1.4.1:
|
||||
resolution: {integrity: sha512-3MoPQQPV1uKyOMVNd6SZfONi+f3st0r8PksexIdBTeIYbMX0Jr+k7pHEDvsXtR4BpCv90/Pv2MovVNhktKrwvw==}
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
rc-resize-observer@1.4.3:
|
||||
resolution: {integrity: sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
rc-util@5.44.4:
|
||||
resolution: {integrity: sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==}
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
react-css-styled@1.1.9:
|
||||
resolution: {integrity: sha512-M7fJZ3IWFaIHcZEkoFOnkjdiUFmwd8d+gTh2bpqMOcnxy/0Gsykw4dsL4QBiKsxcGow6tETUa4NAUcmJF+/nfw==}
|
||||
|
||||
@@ -3639,6 +3744,9 @@ packages:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
resize-observer-polyfill@1.5.1:
|
||||
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -5047,6 +5155,74 @@ snapshots:
|
||||
|
||||
'@radix-ui/rect@1.1.1': {}
|
||||
|
||||
'@rc-component/input@1.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@rc-component/util': 1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
classnames: 2.5.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@rc-component/mentions@1.2.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@rc-component/input': 1.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/menu': 1.1.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/textarea': 1.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/trigger': 3.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/util': 1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
classnames: 2.5.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@rc-component/menu@1.1.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@rc-component/motion': 1.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/trigger': 3.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/util': 1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
classnames: 2.5.1
|
||||
rc-overflow: 1.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@rc-component/motion@1.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@rc-component/util': 1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
classnames: 2.5.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@rc-component/portal@2.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@rc-component/util': 1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
classnames: 2.5.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@rc-component/resize-observer@1.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@rc-component/util': 1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
classnames: 2.5.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@rc-component/textarea@1.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@rc-component/input': 1.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/resize-observer': 1.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/util': 1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
classnames: 2.5.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@rc-component/trigger@3.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@rc-component/motion': 1.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/portal': 2.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/resize-observer': 1.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/util': 1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
classnames: 2.5.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@rc-component/util@1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
@@ -5216,7 +5392,7 @@ snapshots:
|
||||
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
|
||||
'@tiptap/extension-text-style': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
|
||||
'@tiptap/extension-document@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))':
|
||||
'@tiptap/extension-document@2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
|
||||
|
||||
@@ -5276,6 +5452,12 @@ snapshots:
|
||||
dependencies:
|
||||
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
|
||||
|
||||
'@tiptap/extension-mention@2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(@tiptap/suggestion@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
|
||||
'@tiptap/pm': 2.11.7
|
||||
'@tiptap/suggestion': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
|
||||
|
||||
'@tiptap/extension-ordered-list@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
|
||||
@@ -5323,7 +5505,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
|
||||
|
||||
'@tiptap/extension-text@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))':
|
||||
'@tiptap/extension-text@2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
|
||||
|
||||
@@ -5376,7 +5558,7 @@ snapshots:
|
||||
'@tiptap/extension-bullet-list': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-code': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-code-block': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
|
||||
'@tiptap/extension-document': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-document': 2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-dropcursor': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
|
||||
'@tiptap/extension-gapcursor': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
|
||||
'@tiptap/extension-hard-break': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
@@ -5388,7 +5570,7 @@ snapshots:
|
||||
'@tiptap/extension-ordered-list': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-paragraph': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-strike': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-text': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-text': 2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-text-style': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/pm': 2.11.7
|
||||
|
||||
@@ -6106,6 +6288,17 @@ snapshots:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
|
||||
dotenv-cli@8.0.0:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
dotenv: 16.5.0
|
||||
dotenv-expand: 10.0.0
|
||||
minimist: 1.2.8
|
||||
|
||||
dotenv-expand@10.0.0: {}
|
||||
|
||||
dotenv@16.5.0: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -7816,6 +8009,31 @@ snapshots:
|
||||
schema-utils: 3.3.0
|
||||
webpack: 5.99.6
|
||||
|
||||
rc-overflow@1.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.0
|
||||
classnames: 2.5.1
|
||||
rc-resize-observer: 1.4.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
rc-util: 5.44.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
rc-resize-observer@1.4.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.0
|
||||
classnames: 2.5.1
|
||||
rc-util: 5.44.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
resize-observer-polyfill: 1.5.1
|
||||
|
||||
rc-util@5.44.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.0
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
react-is: 18.3.1
|
||||
|
||||
react-css-styled@1.1.9:
|
||||
dependencies:
|
||||
css-styled: 1.0.8
|
||||
@@ -8020,6 +8238,8 @@ snapshots:
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
resize-observer-polyfill@1.5.1: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "reasoning_content": "我需要仔细分析用户的问题。用户想了解什么是vibe coding。这是一个相对较新的概念,我需要收集相关信息来提供全面的答案。"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "reasoning_content": "\n\n首先,我应该理解vibe coding的基本定义和概念。这可能涉及编程文化、开发方法论或者特定的编程风格。"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "reasoning_content": "\n\n然后,我需要研究它的起源、核心理念,以及在实际开发中的应用。这将帮助我提供一个全面而准确的答案。"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "reasoning_content": "\n\n让我思考一下需要收集哪些具体信息:\n1. Vibe coding的定义和起源\n2. 核心理念和哲学\n3. 实际应用场景和案例\n4. 与传统编程方法的区别\n5. 社区和工具支持"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "reasoning_content": "\n\n基于这些思考,我认为需要进行深入的研究来收集足够的信息。现在我将制定一个详细的研究计划。"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "{"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"locale\": \"zh-CN\","}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"has_enough_context\": false,"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"thought\": \"用户想了解vibe coding的概念。"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "由于目前没有足够的信息来全面回答这个问题,"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "我需要收集更多相关数据。\","}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"title\": \"Vibe Coding 概念研究\","}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"steps\": ["}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n {"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"need_search\": true,"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"title\": \"Vibe Coding 基本定义和概念\","}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"description\": \"收集关于vibe coding的基本定义、"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "起源、核心概念和目标的信息。"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "查找官方定义、行业专家的解释以及相关的编程文化背景。\","}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"step_type\": \"research\""}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n },"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n {"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"need_search\": true,"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"title\": \"实际应用案例和最佳实践\","}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"description\": \"研究vibe coding在实际项目中的应用案例,"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "了解最佳实践和常见的实现方法。\","}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n \"step_type\": \"research\""}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n }"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n ]"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "content": "\n}"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "test-thread", "agent": "planner", "id": "test-id", "role": "assistant", "finish_reason": "stop"}
|
||||
|
||||
@@ -23,19 +23,19 @@ event: message_chunk
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": " reason it's trending, and some key statistics (stars, forks, contributors, etc.).\",\n \"title\": \"Research Plan: Top Trending GitHub Repository Today"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": "\",\n \"steps\": [\n {\n \"need_web_search\": true,\n \"title\": \"Identify and Profile the Top Trending Repository\",\n \"description\": \"Identify the #1 trending repository on"}
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": "\",\n \"steps\": [\n {\n \"need_search\": true,\n \"title\": \"Identify and Profile the Top Trending Repository\",\n \"description\": \"Identify the #1 trending repository on"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": " GitHub today. Collect the following information: repository name, repository owner/organization, a short description of the repository's purpose, the primary programming language used, and the reason GitHub marks it as trending (e.g., 'X new stars today"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": "'). Note: ensure to filter for 'today' to get the current trending repo.\",\n \"step_type\": \"research\"\n },\n {\n \"need_web_search\": true,\n \"title\": \"Gather Repository Statistics and Community Data\",\n \"description\": \"Collect"}
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": "'). Note: ensure to filter for 'today' to get the current trending repo.\",\n \"step_type\": \"research\"\n },\n {\n \"need_search\": true,\n \"title\": \"Gather Repository Statistics and Community Data\",\n \"description\": \"Collect"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": " detailed statistics for the top trending repository. This includes the total number of stars, forks, open issues, closed issues, contributors, and recent commit activity. Also, gather data about the community's involvement, such as the number of active contributors in the last month, and any available information on significant discussions or contributions happening"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": " within the project. Check for recent release notes or announcements.\",\n \"step_type\": \"research\"\n },\n {\n \"need_web_search\": true,\n \"title\": \"Determine Context and Significance\",\n \"description\": \"Research the broader context and significance of the trending"}
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": " within the project. Check for recent release notes or announcements.\",\n \"step_type\": \"research\"\n },\n {\n \"need_search\": true,\n \"title\": \"Determine Context and Significance\",\n \"description\": \"Research the broader context and significance of the trending"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": " repository. Determine the repository's purpose or function. Investigate the project's background, the problem it solves, or the features it provides. Identify the industry, user base, or application area it serves. Search for recent news, articles, or blog posts mentioning the repository and its impact or potential. Identify its license"}
|
||||
|
||||
@@ -20,16 +20,16 @@ event: message_chunk
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": " culinary scene and document its traditional dishes. I will create comprehensive steps to gather the most important data and create a good final report.\",\n \"title\": \"Research"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": " Plan: Nanjing's Culinary Scene and Traditional Dishes\",\n \"steps\": [\n {\n \"need_web_search\": true,\n "}
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": " Plan: Nanjing's Culinary Scene and Traditional Dishes\",\n \"steps\": [\n {\n \"need_search\": true,\n "}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": "\"title\": \"Identify and Document Key Traditional Nanjing Dishes\",\n \"description\": \"Research and compile a comprehensive list of traditional Nanjing dishes, including their names (in both English and Chinese), detailed descriptions of ingredients and preparation methods, and historical origins"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": ". Identify dishes that are representative of Nanjing's culinary heritage and those that are less well-known but still significant. Document the specific cooking techniques that characterize Nanjing cuisine.\",\n \"step_type\": \"research\"\n },\n {\n \"need_web_search\": true,\n \"title\": \"Investigate the History and Cultural Significance of Nanjing Cuisine\",\n \"description\": \"Explore the historical influences that have shaped Nanjing's culinary traditions, including its role as a former capital city. Document the cultural significance of specific dishes and"}
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": ". Identify dishes that are representative of Nanjing's culinary heritage and those that are less well-known but still significant. Document the specific cooking techniques that characterize Nanjing cuisine.\",\n \"step_type\": \"research\"\n },\n {\n \"need_search\": true,\n \"title\": \"Investigate the History and Cultural Significance of Nanjing Cuisine\",\n \"description\": \"Explore the historical influences that have shaped Nanjing's culinary traditions, including its role as a former capital city. Document the cultural significance of specific dishes and"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": " their connection to local customs, festivals, and celebrations. Research the evolution of Nanjing cuisine over time, identifying key periods of change and the factors that contributed to them.\",\n \"step_type\": \"research\"\n },\n {\n \"need_web_search\": true,\n \"title\":"}
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": " their connection to local customs, festivals, and celebrations. Research the evolution of Nanjing cuisine over time, identifying key periods of change and the factors that contributed to them.\",\n \"step_type\": \"research\"\n },\n {\n \"need_search\": true,\n \"title\":"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": " \"Analyze the Current State of Nanjing's Culinary Scene and Identify Key Restaurants\",\n \"description\": \"Investigate the current state of Nanjing's culinary scene, identifying key restaurants that specialize in traditional Nanjing cuisine. Gather information on their menus, pricing, and customer reviews. Document any trends or changes in the local food"}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -83,7 +83,7 @@ event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": "\": [\n {\n \""}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": "need_web_search\":"}
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": "need_search\":"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": " true,\n \""}
|
||||
@@ -134,7 +134,7 @@ event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": " {\n \""}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": "need_web_search\":"}
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": "need_search\":"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": " true,\n \"title"}
|
||||
@@ -194,7 +194,7 @@ event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": "\"\n },\n {\n \""}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": "need_web_search\":"}
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": "need_search\":"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": " true,\n \"title"}
|
||||
|
||||
@@ -140,7 +140,7 @@ event: message_chunk
|
||||
data: {"thread_id": "01uPkjxNhUsYZHQ1DrkhK", "agent": "planner", "id": "run-77b32288-ec82-4b8e-b815-d403687915bd", "role": "assistant", "content": "research\"\n },\n {\n"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "01uPkjxNhUsYZHQ1DrkhK", "agent": "planner", "id": "run-77b32288-ec82-4b8e-b815-d403687915bd", "role": "assistant", "content": " \"need_web_search\":"}
|
||||
data: {"thread_id": "01uPkjxNhUsYZHQ1DrkhK", "agent": "planner", "id": "run-77b32288-ec82-4b8e-b815-d403687915bd", "role": "assistant", "content": " \"need_search\":"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "01uPkjxNhUsYZHQ1DrkhK", "agent": "planner", "id": "run-77b32288-ec82-4b8e-b815-d403687915bd", "role": "assistant", "content": " true,\n \"title"}
|
||||
@@ -200,7 +200,7 @@ event: message_chunk
|
||||
data: {"thread_id": "01uPkjxNhUsYZHQ1DrkhK", "agent": "planner", "id": "run-77b32288-ec82-4b8e-b815-d403687915bd", "role": "assistant", "content": "research\"\n },\n {\n"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "01uPkjxNhUsYZHQ1DrkhK", "agent": "planner", "id": "run-77b32288-ec82-4b8e-b815-d403687915bd", "role": "assistant", "content": " \"need_web_search\":"}
|
||||
data: {"thread_id": "01uPkjxNhUsYZHQ1DrkhK", "agent": "planner", "id": "run-77b32288-ec82-4b8e-b815-d403687915bd", "role": "assistant", "content": " \"need_search\":"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "01uPkjxNhUsYZHQ1DrkhK", "agent": "planner", "id": "run-77b32288-ec82-4b8e-b815-d403687915bd", "role": "assistant", "content": " true,\n \"title"}
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { MagicWandIcon } from "@radix-ui/react-icons";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ArrowUp, X } from "lucide-react";
|
||||
import {
|
||||
type KeyboardEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { ArrowUp, Lightbulb, X } from "lucide-react";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { Detective } from "~/components/deer-flow/icons/detective";
|
||||
import MessageInput, {
|
||||
type MessageInputRef,
|
||||
} from "~/components/deer-flow/message-input";
|
||||
import { ReportStyleDialog } from "~/components/deer-flow/report-style-dialog";
|
||||
import { Tooltip } from "~/components/deer-flow/tooltip";
|
||||
import { BorderBeam } from "~/components/magicui/border-beam";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import type { Option } from "~/core/messages";
|
||||
import { enhancePrompt } from "~/core/api";
|
||||
import { getConfig } from "~/core/api/config";
|
||||
import type { Option, Resource } from "~/core/messages";
|
||||
import {
|
||||
setEnableDeepThinking,
|
||||
setEnableBackgroundInvestigation,
|
||||
useSettingsStore,
|
||||
} from "~/core/store";
|
||||
@@ -23,7 +26,6 @@ import { cn } from "~/lib/utils";
|
||||
|
||||
export function InputBox({
|
||||
className,
|
||||
size,
|
||||
responding,
|
||||
feedback,
|
||||
onSend,
|
||||
@@ -34,78 +36,105 @@ export function InputBox({
|
||||
size?: "large" | "normal";
|
||||
responding?: boolean;
|
||||
feedback?: { option: Option } | null;
|
||||
onSend?: (message: string, options?: { interruptFeedback?: string }) => void;
|
||||
onSend?: (
|
||||
message: string,
|
||||
options?: {
|
||||
interruptFeedback?: string;
|
||||
resources?: Array<Resource>;
|
||||
},
|
||||
) => void;
|
||||
onCancel?: () => void;
|
||||
onRemoveFeedback?: () => void;
|
||||
}) {
|
||||
const [message, setMessage] = useState("");
|
||||
const [imeStatus, setImeStatus] = useState<"active" | "inactive">("inactive");
|
||||
const [indent, setIndent] = useState(0);
|
||||
const enableDeepThinking = useSettingsStore(
|
||||
(state) => state.general.enableDeepThinking,
|
||||
);
|
||||
const backgroundInvestigation = useSettingsStore(
|
||||
(state) => state.general.enableBackgroundInvestigation,
|
||||
);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const reasoningModel = useMemo(() => getConfig().models.reasoning?.[0], []);
|
||||
const reportStyle = useSettingsStore((state) => state.general.reportStyle);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<MessageInputRef>(null);
|
||||
const feedbackRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (feedback) {
|
||||
setMessage("");
|
||||
// Enhancement state
|
||||
const [isEnhancing, setIsEnhancing] = useState(false);
|
||||
const [isEnhanceAnimating, setIsEnhanceAnimating] = useState(false);
|
||||
const [currentPrompt, setCurrentPrompt] = useState("");
|
||||
|
||||
setTimeout(() => {
|
||||
if (feedbackRef.current) {
|
||||
setIndent(feedbackRef.current.offsetWidth);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
setTimeout(() => {
|
||||
textareaRef.current?.focus();
|
||||
}, 0);
|
||||
}, [feedback]);
|
||||
|
||||
const handleSendMessage = useCallback(() => {
|
||||
if (responding) {
|
||||
onCancel?.();
|
||||
} else {
|
||||
if (message.trim() === "") {
|
||||
return;
|
||||
}
|
||||
if (onSend) {
|
||||
onSend(message, {
|
||||
interruptFeedback: feedback?.option.value,
|
||||
});
|
||||
setMessage("");
|
||||
onRemoveFeedback?.();
|
||||
}
|
||||
}
|
||||
}, [responding, onCancel, message, onSend, feedback, onRemoveFeedback]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const handleSendMessage = useCallback(
|
||||
(message: string, resources: Array<Resource>) => {
|
||||
if (responding) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
!event.shiftKey &&
|
||||
!event.metaKey &&
|
||||
!event.ctrlKey &&
|
||||
imeStatus === "inactive"
|
||||
) {
|
||||
event.preventDefault();
|
||||
handleSendMessage();
|
||||
onCancel?.();
|
||||
} else {
|
||||
if (message.trim() === "") {
|
||||
return;
|
||||
}
|
||||
if (onSend) {
|
||||
onSend(message, {
|
||||
interruptFeedback: feedback?.option.value,
|
||||
resources,
|
||||
});
|
||||
onRemoveFeedback?.();
|
||||
// Clear enhancement animation after sending
|
||||
setIsEnhanceAnimating(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[responding, imeStatus, handleSendMessage],
|
||||
[responding, onCancel, onSend, feedback, onRemoveFeedback],
|
||||
);
|
||||
|
||||
const handleEnhancePrompt = useCallback(async () => {
|
||||
if (currentPrompt.trim() === "" || isEnhancing) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsEnhancing(true);
|
||||
setIsEnhanceAnimating(true);
|
||||
|
||||
try {
|
||||
const enhancedPrompt = await enhancePrompt({
|
||||
prompt: currentPrompt,
|
||||
report_style: reportStyle.toUpperCase(),
|
||||
});
|
||||
|
||||
// Add a small delay for better UX
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Update the input with the enhanced prompt with animation
|
||||
if (inputRef.current) {
|
||||
inputRef.current.setContent(enhancedPrompt);
|
||||
setCurrentPrompt(enhancedPrompt);
|
||||
}
|
||||
|
||||
// Keep animation for a bit longer to show the effect
|
||||
setTimeout(() => {
|
||||
setIsEnhanceAnimating(false);
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error("Failed to enhance prompt:", error);
|
||||
setIsEnhanceAnimating(false);
|
||||
// Could add toast notification here
|
||||
} finally {
|
||||
setIsEnhancing(false);
|
||||
}
|
||||
}, [currentPrompt, isEnhancing, reportStyle]);
|
||||
|
||||
return (
|
||||
<div className={cn("bg-card relative rounded-[24px] border", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"bg-card relative flex h-full w-full flex-col rounded-[24px] border",
|
||||
className,
|
||||
)}
|
||||
ref={containerRef}
|
||||
>
|
||||
<div className="w-full">
|
||||
<AnimatePresence>
|
||||
{feedback && (
|
||||
<motion.div
|
||||
ref={feedbackRef}
|
||||
className="bg-background border-brand absolute top-0 left-0 mt-3 ml-2 flex items-center justify-center gap-1 rounded-2xl border px-2 py-0.5"
|
||||
className="bg-background border-brand absolute top-0 left-0 mt-2 ml-4 flex items-center justify-center gap-1 rounded-2xl border px-2 py-0.5"
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0 }}
|
||||
@@ -121,30 +150,95 @@ export function InputBox({
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={cn(
|
||||
"m-0 w-full resize-none border-none px-4 py-3 text-lg",
|
||||
size === "large" ? "min-h-32" : "min-h-4",
|
||||
{isEnhanceAnimating && (
|
||||
<motion.div
|
||||
className="pointer-events-none absolute inset-0 z-20"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="relative h-full w-full">
|
||||
{/* Sparkle effect overlay */}
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-[24px] bg-gradient-to-r from-blue-500/10 via-purple-500/10 to-blue-500/10"
|
||||
animate={{
|
||||
background: [
|
||||
"linear-gradient(45deg, rgba(59, 130, 246, 0.1), rgba(147, 51, 234, 0.1), rgba(59, 130, 246, 0.1))",
|
||||
"linear-gradient(225deg, rgba(147, 51, 234, 0.1), rgba(59, 130, 246, 0.1), rgba(147, 51, 234, 0.1))",
|
||||
"linear-gradient(45deg, rgba(59, 130, 246, 0.1), rgba(147, 51, 234, 0.1), rgba(59, 130, 246, 0.1))",
|
||||
],
|
||||
}}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
/>
|
||||
{/* Floating sparkles */}
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="absolute h-2 w-2 rounded-full bg-blue-400"
|
||||
style={{
|
||||
left: `${20 + i * 12}%`,
|
||||
top: `${30 + (i % 2) * 40}%`,
|
||||
}}
|
||||
animate={{
|
||||
y: [-10, -20, -10],
|
||||
opacity: [0, 1, 0],
|
||||
scale: [0.5, 1, 0.5],
|
||||
}}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
repeat: Infinity,
|
||||
delay: i * 0.2,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
style={{ textIndent: feedback ? `${indent}px` : 0 }}
|
||||
placeholder={
|
||||
feedback
|
||||
? `Describe how you ${feedback.option.text.toLocaleLowerCase()}?`
|
||||
: "What can I do for you?"
|
||||
}
|
||||
value={message}
|
||||
onCompositionStart={() => setImeStatus("active")}
|
||||
onCompositionEnd={() => setImeStatus("inactive")}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={(event) => {
|
||||
setMessage(event.target.value);
|
||||
}}
|
||||
</AnimatePresence>
|
||||
<MessageInput
|
||||
className={cn(
|
||||
"h-24 px-4 pt-5",
|
||||
feedback && "pt-9",
|
||||
isEnhanceAnimating && "transition-all duration-500",
|
||||
)}
|
||||
ref={inputRef}
|
||||
onEnter={handleSendMessage}
|
||||
onChange={setCurrentPrompt}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center px-4 py-2">
|
||||
<div className="flex grow">
|
||||
<div className="flex grow gap-2">
|
||||
{reasoningModel && (
|
||||
<Tooltip
|
||||
className="max-w-60"
|
||||
title={
|
||||
<div>
|
||||
<h3 className="mb-2 font-bold">
|
||||
Deep Thinking Mode: {enableDeepThinking ? "On" : "Off"}
|
||||
</h3>
|
||||
<p>
|
||||
When enabled, DeerFlow will use reasoning model (
|
||||
{reasoningModel}) to generate more thoughtful plans.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className={cn(
|
||||
"rounded-2xl",
|
||||
enableDeepThinking && "!border-brand !text-brand",
|
||||
)}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setEnableDeepThinking(!enableDeepThinking);
|
||||
}}
|
||||
>
|
||||
<Lightbulb /> Deep Thinking
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip
|
||||
className="max-w-60"
|
||||
title={
|
||||
@@ -166,7 +260,6 @@ export function InputBox({
|
||||
backgroundInvestigation && "!border-brand !text-brand",
|
||||
)}
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={() =>
|
||||
setEnableBackgroundInvestigation(!backgroundInvestigation)
|
||||
}
|
||||
@@ -174,14 +267,35 @@ export function InputBox({
|
||||
<Detective /> Investigation
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<ReportStyleDialog />
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Tooltip title="Enhance prompt with AI">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"hover:bg-accent h-10 w-10",
|
||||
isEnhancing && "animate-pulse",
|
||||
)}
|
||||
onClick={handleEnhancePrompt}
|
||||
disabled={isEnhancing || currentPrompt.trim() === ""}
|
||||
>
|
||||
{isEnhancing ? (
|
||||
<div className="flex h-10 w-10 items-center justify-center">
|
||||
<div className="bg-foreground h-3 w-3 animate-bounce rounded-full opacity-70" />
|
||||
</div>
|
||||
) : (
|
||||
<MagicWandIcon className="text-brand" />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title={responding ? "Stop" : "Send"}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn("h-10 w-10 rounded-full")}
|
||||
onClick={handleSendMessage}
|
||||
onClick={() => inputRef.current?.submit()}
|
||||
>
|
||||
{responding ? (
|
||||
<div className="flex h-10 w-10 items-center justify-center">
|
||||
@@ -194,6 +308,21 @@ export function InputBox({
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{isEnhancing && (
|
||||
<>
|
||||
<BorderBeam
|
||||
duration={5}
|
||||
size={250}
|
||||
className="from-transparent via-red-500 to-transparent"
|
||||
/>
|
||||
<BorderBeam
|
||||
duration={5}
|
||||
delay={3}
|
||||
size={250}
|
||||
className="from-transparent via-blue-500 to-transparent"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,14 @@
|
||||
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import { motion } from "framer-motion";
|
||||
import { Download, Headphones } from "lucide-react";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Download,
|
||||
Headphones,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Lightbulb,
|
||||
} from "lucide-react";
|
||||
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { LoadingAnimation } from "~/components/deer-flow/loading-animation";
|
||||
import { Markdown } from "~/components/deer-flow/markdown";
|
||||
@@ -23,6 +29,11 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "~/components/ui/collapsible";
|
||||
import type { Message, Option } from "~/core/messages";
|
||||
import {
|
||||
closeResearch,
|
||||
@@ -173,8 +184,15 @@ function MessageListItem({
|
||||
)}
|
||||
>
|
||||
<MessageBubble message={message}>
|
||||
<div className="flex w-full flex-col">
|
||||
<Markdown>{message?.content}</Markdown>
|
||||
<div className="flex w-full flex-col text-wrap break-words">
|
||||
<Markdown
|
||||
className={cn(
|
||||
message.role === "user" &&
|
||||
"prose-invert not-dark:text-secondary dark:text-inherit",
|
||||
)}
|
||||
>
|
||||
{message?.content}
|
||||
</Markdown>
|
||||
</div>
|
||||
</MessageBubble>
|
||||
</div>
|
||||
@@ -214,9 +232,8 @@ function MessageBubble({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
`flex w-fit max-w-[85%] flex-col rounded-2xl px-4 py-3 shadow`,
|
||||
message.role === "user" &&
|
||||
"text-primary-foreground bg-brand rounded-ee-none",
|
||||
`group flex w-fit max-w-[85%] flex-col rounded-2xl px-4 py-3 text-nowrap shadow`,
|
||||
message.role === "user" && "bg-brand rounded-ee-none",
|
||||
message.role === "assistant" && "bg-card rounded-es-none",
|
||||
className,
|
||||
)}
|
||||
@@ -288,6 +305,114 @@ function ResearchCard({
|
||||
);
|
||||
}
|
||||
|
||||
function ThoughtBlock({
|
||||
className,
|
||||
content,
|
||||
isStreaming,
|
||||
hasMainContent,
|
||||
}: {
|
||||
className?: string;
|
||||
content: string;
|
||||
isStreaming?: boolean;
|
||||
hasMainContent?: boolean;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const [hasAutoCollapsed, setHasAutoCollapsed] = useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasMainContent && !hasAutoCollapsed) {
|
||||
setIsOpen(false);
|
||||
setHasAutoCollapsed(true);
|
||||
}
|
||||
}, [hasMainContent, hasAutoCollapsed]);
|
||||
|
||||
if (!content || content.trim() === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("mb-6 w-full", className)}>
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"h-auto w-full justify-start rounded-xl border px-6 py-4 text-left transition-all duration-200",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
isStreaming
|
||||
? "border-primary/20 bg-primary/5 shadow-sm"
|
||||
: "border-border bg-card",
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center gap-3">
|
||||
<Lightbulb
|
||||
size={18}
|
||||
className={cn(
|
||||
"shrink-0 transition-colors duration-200",
|
||||
isStreaming ? "text-primary" : "text-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"leading-none font-semibold transition-colors duration-200",
|
||||
isStreaming ? "text-primary" : "text-foreground",
|
||||
)}
|
||||
>
|
||||
Deep Thinking
|
||||
</span>
|
||||
{isStreaming && <LoadingAnimation className="ml-2 scale-75" />}
|
||||
<div className="flex-grow" />
|
||||
{isOpen ? (
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className="text-muted-foreground transition-transform duration-200"
|
||||
/>
|
||||
) : (
|
||||
<ChevronRight
|
||||
size={16}
|
||||
className="text-muted-foreground transition-transform duration-200"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-up-2 data-[state=open]:slide-down-2 mt-3">
|
||||
<Card
|
||||
className={cn(
|
||||
"transition-all duration-200",
|
||||
isStreaming ? "border-primary/20 bg-primary/5" : "border-border",
|
||||
)}
|
||||
>
|
||||
<CardContent>
|
||||
<div className="flex h-40 w-full overflow-y-auto">
|
||||
<ScrollContainer
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
scrollShadow={false}
|
||||
autoScrollToBottom
|
||||
>
|
||||
<Markdown
|
||||
className={cn(
|
||||
"prose dark:prose-invert max-w-none transition-colors duration-200",
|
||||
isStreaming ? "prose-primary" : "opacity-80",
|
||||
)}
|
||||
animated={isStreaming}
|
||||
>
|
||||
{content}
|
||||
</Markdown>
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const GREETINGS = ["Cool", "Sounds great", "Looks good", "Great", "Awesome"];
|
||||
function PlanCard({
|
||||
className,
|
||||
@@ -314,6 +439,17 @@ function PlanCard({
|
||||
}>(() => {
|
||||
return parseJSON(message.content ?? "", {});
|
||||
}, [message.content]);
|
||||
|
||||
const reasoningContent = message.reasoningContent;
|
||||
const hasMainContent = Boolean(
|
||||
message.content && message.content.trim() !== "",
|
||||
);
|
||||
|
||||
// 判断是否正在思考:有推理内容但还没有主要内容
|
||||
const isThinking = Boolean(reasoningContent && !hasMainContent);
|
||||
|
||||
// 判断是否应该显示计划:有主要内容就显示(无论是否还在流式传输)
|
||||
const shouldShowPlan = hasMainContent;
|
||||
const handleAccept = useCallback(async () => {
|
||||
if (onSendMessage) {
|
||||
onSendMessage(
|
||||
@@ -325,67 +461,90 @@ function PlanCard({
|
||||
}
|
||||
}, [onSendMessage]);
|
||||
return (
|
||||
<Card className={cn("w-full", className)}>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Markdown animated>
|
||||
{`### ${
|
||||
plan.title !== undefined && plan.title !== ""
|
||||
? plan.title
|
||||
: "Deep Research"
|
||||
}`}
|
||||
</Markdown>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Markdown className="opacity-80" animated>
|
||||
{plan.thought}
|
||||
</Markdown>
|
||||
{plan.steps && (
|
||||
<ul className="my-2 flex list-decimal flex-col gap-4 border-l-[2px] pl-8">
|
||||
{plan.steps.map((step, i) => (
|
||||
<li key={`step-${i}`}>
|
||||
<h3 className="mb text-lg font-medium">
|
||||
<Markdown animated>{step.title}</Markdown>
|
||||
</h3>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
<Markdown animated>{step.description}</Markdown>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-end">
|
||||
{!message.isStreaming && interruptMessage?.options?.length && (
|
||||
<motion.div
|
||||
className="flex gap-2"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.3 }}
|
||||
>
|
||||
{interruptMessage?.options.map((option) => (
|
||||
<Button
|
||||
key={option.value}
|
||||
variant={option.value === "accepted" ? "default" : "outline"}
|
||||
disabled={!waitForFeedback}
|
||||
onClick={() => {
|
||||
if (option.value === "accepted") {
|
||||
void handleAccept();
|
||||
} else {
|
||||
onFeedback?.({
|
||||
option,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{option.text}
|
||||
</Button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<div className={cn("w-full", className)}>
|
||||
{reasoningContent && (
|
||||
<ThoughtBlock
|
||||
content={reasoningContent}
|
||||
isStreaming={isThinking}
|
||||
hasMainContent={hasMainContent}
|
||||
/>
|
||||
)}
|
||||
{shouldShowPlan && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Markdown animated={message.isStreaming}>
|
||||
{`### ${
|
||||
plan.title !== undefined && plan.title !== ""
|
||||
? plan.title
|
||||
: "Deep Research"
|
||||
}`}
|
||||
</Markdown>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Markdown className="opacity-80" animated={message.isStreaming}>
|
||||
{plan.thought}
|
||||
</Markdown>
|
||||
{plan.steps && (
|
||||
<ul className="my-2 flex list-decimal flex-col gap-4 border-l-[2px] pl-8">
|
||||
{plan.steps.map((step, i) => (
|
||||
<li key={`step-${i}`}>
|
||||
<h3 className="mb text-lg font-medium">
|
||||
<Markdown animated={message.isStreaming}>
|
||||
{step.title}
|
||||
</Markdown>
|
||||
</h3>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
<Markdown animated={message.isStreaming}>
|
||||
{step.description}
|
||||
</Markdown>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-end">
|
||||
{!message.isStreaming && interruptMessage?.options?.length && (
|
||||
<motion.div
|
||||
className="flex gap-2"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.3 }}
|
||||
>
|
||||
{interruptMessage?.options.map((option) => (
|
||||
<Button
|
||||
key={option.value}
|
||||
variant={
|
||||
option.value === "accepted" ? "default" : "outline"
|
||||
}
|
||||
disabled={!waitForFeedback}
|
||||
onClick={() => {
|
||||
if (option.value === "accepted") {
|
||||
void handleAccept();
|
||||
} else {
|
||||
onFeedback?.({
|
||||
option,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{option.text}
|
||||
</Button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "~/components/ui/card";
|
||||
import { fastForwardReplay } from "~/core/api";
|
||||
import { useReplayMetadata } from "~/core/api/hooks";
|
||||
import type { Option } from "~/core/messages";
|
||||
import type { Option, Resource } from "~/core/messages";
|
||||
import { useReplay } from "~/core/replay";
|
||||
import { sendMessage, useMessageIds, useStore } from "~/core/store";
|
||||
import { env } from "~/env";
|
||||
@@ -36,7 +36,13 @@ export function MessagesBlock({ className }: { className?: string }) {
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const [feedback, setFeedback] = useState<{ option: Option } | null>(null);
|
||||
const handleSend = useCallback(
|
||||
async (message: string, options?: { interruptFeedback?: string }) => {
|
||||
async (
|
||||
message: string,
|
||||
options?: {
|
||||
interruptFeedback?: string;
|
||||
resources?: Array<Resource>;
|
||||
},
|
||||
) => {
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
try {
|
||||
@@ -45,6 +51,7 @@ export function MessagesBlock({ className }: { className?: string }) {
|
||||
{
|
||||
interruptFeedback:
|
||||
options?.interruptFeedback ?? feedback?.option.value,
|
||||
resources: options?.resources,
|
||||
},
|
||||
{
|
||||
abortSignal: abortController.signal,
|
||||
@@ -123,8 +130,26 @@ export function MessagesBlock({ className }: { className?: string }) {
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-grow">
|
||||
<CardHeader>
|
||||
<div className="flex flex-grow items-center">
|
||||
{responding && (
|
||||
<motion.div
|
||||
className="ml-3"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<video
|
||||
// Walking deer animation, designed by @liangzhaojun. Thank you for creating it!
|
||||
src="/images/walking_deer.webm"
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
className="h-[42px] w-[42px] object-contain"
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
<CardHeader className={cn("flex-grow", responding && "pl-3")}>
|
||||
<CardTitle>
|
||||
<RainbowText animated={responding}>
|
||||
{responding ? "Replaying" : `${replayTitle}`}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { PythonOutlined } from "@ant-design/icons";
|
||||
import { motion } from "framer-motion";
|
||||
import { LRUCache } from "lru-cache";
|
||||
import { BookOpenText, PencilRuler, Search } from "lucide-react";
|
||||
import { BookOpenText, FileText, PencilRuler, Search } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useMemo } from "react";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
@@ -96,6 +96,8 @@ function ActivityListItem({ messageId }: { messageId: string }) {
|
||||
return <CrawlToolCall key={toolCall.id} toolCall={toolCall} />;
|
||||
} else if (toolCall.name === "python_repl_tool") {
|
||||
return <PythonToolCall key={toolCall.id} toolCall={toolCall} />;
|
||||
} else if (toolCall.name === "local_search_tool") {
|
||||
return <RetrieverToolCall key={toolCall.id} toolCall={toolCall} />;
|
||||
} else {
|
||||
return <MCPToolCall key={toolCall.id} toolCall={toolCall} />;
|
||||
}
|
||||
@@ -118,6 +120,7 @@ type SearchResult =
|
||||
image_url: string;
|
||||
image_description: string;
|
||||
};
|
||||
|
||||
function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
const searching = useMemo(() => {
|
||||
return toolCall.result === undefined;
|
||||
@@ -275,9 +278,67 @@ function CrawlToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
);
|
||||
}
|
||||
|
||||
function RetrieverToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
const searching = useMemo(() => {
|
||||
return toolCall.result === undefined;
|
||||
}, [toolCall.result]);
|
||||
const documents = useMemo<
|
||||
Array<{ id: string; title: string; content: string }>
|
||||
>(() => {
|
||||
return toolCall.result ? parseJSON(toolCall.result, []) : [];
|
||||
}, [toolCall.result]);
|
||||
return (
|
||||
<section className="mt-4 pl-4">
|
||||
<div className="font-medium italic">
|
||||
<RainbowText className="flex items-center" animated={searching}>
|
||||
<Search size={16} className={"mr-2"} />
|
||||
<span>Retrieving documents from RAG </span>
|
||||
<span className="max-w-[500px] overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{(toolCall.args as { keywords: string }).keywords}
|
||||
</span>
|
||||
</RainbowText>
|
||||
</div>
|
||||
<div className="pr-4">
|
||||
{documents && (
|
||||
<ul className="mt-2 flex flex-wrap gap-4">
|
||||
{searching &&
|
||||
[...Array(2)].map((_, i) => (
|
||||
<li
|
||||
key={`search-result-${i}`}
|
||||
className="flex h-40 w-40 gap-2 rounded-md text-sm"
|
||||
>
|
||||
<Skeleton
|
||||
className="to-accent h-full w-full rounded-md bg-gradient-to-tl from-slate-400"
|
||||
style={{ animationDelay: `${i * 0.2}s` }}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
{documents.map((doc, i) => (
|
||||
<motion.li
|
||||
key={`search-result-${i}`}
|
||||
className="text-muted-foreground bg-accent flex max-w-40 gap-2 rounded-md px-2 py-1 text-sm"
|
||||
initial={{ opacity: 0, y: 10, scale: 0.66 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
delay: i * 0.1,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
>
|
||||
<FileText size={32} />
|
||||
{doc.title}
|
||||
</motion.li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
const code = useMemo<string>(() => {
|
||||
return (toolCall.args as { code: string }).code;
|
||||
const code = useMemo<string | undefined>(() => {
|
||||
return (toolCall.args as { code?: string }).code;
|
||||
}, [toolCall.args]);
|
||||
const { resolvedTheme } = useTheme();
|
||||
return (
|
||||
@@ -302,7 +363,7 @@ function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
boxShadow: "none",
|
||||
}}
|
||||
>
|
||||
{code.trim()}
|
||||
{code?.trim() ?? ""}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Check, Copy, Headphones, Pencil, Undo2, X } from "lucide-react";
|
||||
import { Check, Copy, Headphones, Pencil, Undo2, X, Download } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { ScrollContainer } from "~/components/deer-flow/scroll-container";
|
||||
@@ -64,6 +64,33 @@ export function ResearchBlock({
|
||||
}, 1000);
|
||||
}, [reportId]);
|
||||
|
||||
// Download report as markdown
|
||||
const handleDownload = useCallback(() => {
|
||||
if (!reportId) {
|
||||
return;
|
||||
}
|
||||
const report = useStore.getState().messages.get(reportId);
|
||||
if (!report) {
|
||||
return;
|
||||
}
|
||||
const now = new Date();
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
|
||||
const filename = `research-report-${timestamp}.md`;
|
||||
const blob = new Blob([report.content], { type: 'text/markdown' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
}, [reportId]);
|
||||
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
setEditing((editing) => !editing);
|
||||
}, []);
|
||||
@@ -113,6 +140,16 @@ export function ResearchBlock({
|
||||
{copied ? <Check /> : <Copy />}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Download report as markdown">
|
||||
<Button
|
||||
className="text-gray-400"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<Download />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
<Tooltip title="Close">
|
||||
|
||||
@@ -53,10 +53,7 @@ export function ResearchReportBlock({
|
||||
// }, [isCompleted]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={cn("relative flex flex-col pt-4 pb-8", className)}
|
||||
>
|
||||
<div ref={contentRef} className={cn("w-full pt-4 pb-8", className)}>
|
||||
{!isReplay && isCompleted && editing ? (
|
||||
<ReportEditor
|
||||
content={message?.content}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Geist } from "next/font/google";
|
||||
import Script from "next/script";
|
||||
|
||||
import { ThemeProviderWrapper } from "~/components/deer-flow/theme-provider-wrapper";
|
||||
import { loadConfig } from "~/core/api/config";
|
||||
import { env } from "~/env";
|
||||
|
||||
import { Toaster } from "../components/deer-flow/toaster";
|
||||
@@ -24,12 +25,14 @@ const geist = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
});
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
const conf = await loadConfig();
|
||||
return (
|
||||
<html lang="en" className={`${geist.variable}`} suppressHydrationWarning>
|
||||
<head>
|
||||
<script>{`window.__deerflowConfig = ${JSON.stringify(conf)}`}</script>
|
||||
{/* Define isSpace function globally to fix markdown-it issues with Next.js + Turbopack
|
||||
https://github.com/markdown-it/markdown-it/issues/1082#issuecomment-2749656365 */}
|
||||
<Script id="markdown-it-fix" strategy="beforeInteractive">
|
||||
|
||||
@@ -25,7 +25,6 @@ import type { Tab } from "./types";
|
||||
|
||||
const generalFormSchema = z.object({
|
||||
autoAcceptedPlan: z.boolean(),
|
||||
enableBackgroundInvestigation: z.boolean(),
|
||||
maxPlanIterations: z.number().min(1, {
|
||||
message: "Max plan iterations must be at least 1.",
|
||||
}),
|
||||
@@ -35,6 +34,10 @@ const generalFormSchema = z.object({
|
||||
maxSearchResults: z.number().min(1, {
|
||||
message: "Max search results must be at least 1.",
|
||||
}),
|
||||
// Others
|
||||
enableBackgroundInvestigation: z.boolean(),
|
||||
enableDeepThinking: z.boolean(),
|
||||
reportStyle: z.enum(["academic", "popular_science", "news", "social_media"]),
|
||||
});
|
||||
|
||||
export const GeneralTab: Tab = ({
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function Enhance(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M12 2L13.09 8.26L20 9L13.09 9.74L12 16L10.91 9.74L4 9L10.91 8.26L12 2Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M19 14L19.5 16.5L22 17L19.5 17.5L19 20L18.5 17.5L16 17L18.5 16.5L19 14Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M5 6L5.5 7.5L7 8L5.5 8.5L5 10L4.5 8.5L3 8L4.5 7.5L5 6Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
export function ReportStyle({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
version="1.1"
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<g fill="currentcolor">
|
||||
<path
|
||||
d="M4 4C4 3.44772 4.44772 3 5 3H19C19.5523 3 20 3.44772 20 4V20C20 20.5523 19.5523 21 19 21H5C4.44772 21 4 20.5523 4 20V4Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M8 7H16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M8 11H16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M8 15H12"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<circle
|
||||
cx="16"
|
||||
cy="15"
|
||||
r="2"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export const Link = ({
|
||||
}, [credibleLinks, href, responding, checkLinkCredibility]);
|
||||
|
||||
return (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<a href={href} target="_blank" rel="noopener noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
"use client";
|
||||
|
||||
import Mention from "@tiptap/extension-mention";
|
||||
import { Editor, Extension, type Content } from "@tiptap/react";
|
||||
import {
|
||||
EditorContent,
|
||||
type EditorInstance,
|
||||
EditorRoot,
|
||||
type JSONContent,
|
||||
StarterKit,
|
||||
Placeholder,
|
||||
} from "novel";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
import "~/styles/prosemirror.css";
|
||||
import { resourceSuggestion } from "./resource-suggestion";
|
||||
import React, { forwardRef, useEffect, useMemo, useRef } from "react";
|
||||
import type { Resource } from "~/core/messages";
|
||||
import { useRAGProvider } from "~/core/api/hooks";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
|
||||
export interface MessageInputRef {
|
||||
focus: () => void;
|
||||
submit: () => void;
|
||||
setContent: (content: string) => void;
|
||||
}
|
||||
|
||||
export interface MessageInputProps {
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
onChange?: (markdown: string) => void;
|
||||
onEnter?: (message: string, resources: Array<Resource>) => void;
|
||||
}
|
||||
|
||||
function formatMessage(content: JSONContent) {
|
||||
if (content.content) {
|
||||
const output: {
|
||||
text: string;
|
||||
resources: Array<Resource>;
|
||||
} = {
|
||||
text: "",
|
||||
resources: [],
|
||||
};
|
||||
for (const node of content.content) {
|
||||
const { text, resources } = formatMessage(node);
|
||||
output.text += text;
|
||||
output.resources.push(...resources);
|
||||
}
|
||||
return output;
|
||||
} else {
|
||||
return formatItem(content);
|
||||
}
|
||||
}
|
||||
|
||||
function formatItem(item: JSONContent): {
|
||||
text: string;
|
||||
resources: Array<Resource>;
|
||||
} {
|
||||
if (item.type === "text") {
|
||||
return { text: item.text ?? "", resources: [] };
|
||||
}
|
||||
if (item.type === "mention") {
|
||||
return {
|
||||
text: `[${item.attrs?.label}](${item.attrs?.id})`,
|
||||
resources: [
|
||||
{ uri: item.attrs?.id ?? "", title: item.attrs?.label ?? "" },
|
||||
],
|
||||
};
|
||||
}
|
||||
return { text: "", resources: [] };
|
||||
}
|
||||
|
||||
const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
({ className, onChange, onEnter }: MessageInputProps, ref) => {
|
||||
const editorRef = useRef<Editor>(null);
|
||||
const handleEnterRef = useRef<
|
||||
((message: string, resources: Array<Resource>) => void) | undefined
|
||||
>(onEnter);
|
||||
const debouncedUpdates = useDebouncedCallback(
|
||||
async (editor: EditorInstance) => {
|
||||
if (onChange) {
|
||||
// Get the plain text content for prompt enhancement
|
||||
const { text } = formatMessage(editor.getJSON() ?? []);
|
||||
onChange(text);
|
||||
}
|
||||
},
|
||||
200,
|
||||
);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
editorRef.current?.view.focus();
|
||||
},
|
||||
submit: () => {
|
||||
if (onEnter) {
|
||||
const { text, resources } = formatMessage(
|
||||
editorRef.current?.getJSON() ?? [],
|
||||
);
|
||||
onEnter(text, resources);
|
||||
}
|
||||
editorRef.current?.commands.clearContent();
|
||||
},
|
||||
setContent: (content: string) => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.commands.setContent(content);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
handleEnterRef.current = onEnter;
|
||||
}, [onEnter]);
|
||||
|
||||
const { provider, loading } = useRAGProvider();
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
const extensions = [
|
||||
StarterKit,
|
||||
Markdown.configure({
|
||||
html: true,
|
||||
tightLists: true,
|
||||
tightListClass: "tight",
|
||||
bulletListMarker: "-",
|
||||
linkify: false,
|
||||
breaks: false,
|
||||
transformPastedText: false,
|
||||
transformCopiedText: false,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
showOnlyCurrent: false,
|
||||
placeholder: provider
|
||||
? "What can I do for you? \nYou may refer to RAG resources by using @."
|
||||
: "What can I do for you?",
|
||||
emptyEditorClass: "placeholder",
|
||||
}),
|
||||
Extension.create({
|
||||
name: "keyboardHandler",
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
Enter: () => {
|
||||
if (handleEnterRef.current) {
|
||||
const { text, resources } = formatMessage(
|
||||
this.editor.getJSON() ?? [],
|
||||
);
|
||||
handleEnterRef.current(text, resources);
|
||||
}
|
||||
return this.editor.commands.clearContent();
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
];
|
||||
if (provider) {
|
||||
extensions.push(
|
||||
Mention.configure({
|
||||
HTMLAttributes: {
|
||||
class: "mention",
|
||||
},
|
||||
suggestion: resourceSuggestion,
|
||||
}) as Extension,
|
||||
);
|
||||
}
|
||||
return extensions;
|
||||
}, [provider]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<LoadingOutlined />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<EditorRoot>
|
||||
<EditorContent
|
||||
immediatelyRender={false}
|
||||
extensions={extensions}
|
||||
className="border-muted h-full w-full overflow-auto"
|
||||
editorProps={{
|
||||
attributes: {
|
||||
class:
|
||||
"prose prose-base dark:prose-invert inline-editor font-default focus:outline-none max-w-full",
|
||||
},
|
||||
transformPastedHTML: transformPastedHTML,
|
||||
}}
|
||||
onCreate={({ editor }) => {
|
||||
editorRef.current = editor;
|
||||
}}
|
||||
onUpdate={({ editor }) => {
|
||||
debouncedUpdates(editor);
|
||||
}}
|
||||
></EditorContent>
|
||||
</EditorRoot>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function transformPastedHTML(html: string) {
|
||||
try {
|
||||
// Strip HTML from user-pasted content
|
||||
const tempEl = document.createElement("div");
|
||||
tempEl.innerHTML = html;
|
||||
|
||||
return tempEl.textContent || tempEl.innerText || "";
|
||||
} catch (error) {
|
||||
console.error("Error transforming pasted HTML", error);
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export default MessageInput;
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { useState } from "react";
|
||||
import { Check, FileText, Newspaper, Users, GraduationCap } from "lucide-react";
|
||||
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "~/components/ui/dialog";
|
||||
import { setReportStyle, useSettingsStore } from "~/core/store";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
import { Tooltip } from "./tooltip";
|
||||
|
||||
const REPORT_STYLES = [
|
||||
{
|
||||
value: "academic" as const,
|
||||
label: "Academic",
|
||||
description: "Formal, objective, and analytical with precise terminology",
|
||||
icon: GraduationCap,
|
||||
},
|
||||
{
|
||||
value: "popular_science" as const,
|
||||
label: "Popular Science",
|
||||
description: "Engaging and accessible for general audience",
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
value: "news" as const,
|
||||
label: "News",
|
||||
description: "Factual, concise, and impartial journalistic style",
|
||||
icon: Newspaper,
|
||||
},
|
||||
{
|
||||
value: "social_media" as const,
|
||||
label: "Social Media",
|
||||
description: "Concise, attention-grabbing, and shareable",
|
||||
icon: Users,
|
||||
},
|
||||
];
|
||||
|
||||
export function ReportStyleDialog() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const currentStyle = useSettingsStore((state) => state.general.reportStyle);
|
||||
|
||||
const handleStyleChange = (
|
||||
style: "academic" | "popular_science" | "news" | "social_media",
|
||||
) => {
|
||||
setReportStyle(style);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const currentStyleConfig =
|
||||
REPORT_STYLES.find((style) => style.value === currentStyle) ||
|
||||
REPORT_STYLES[0]!;
|
||||
const CurrentIcon = currentStyleConfig.icon;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Tooltip
|
||||
className="max-w-60"
|
||||
title={
|
||||
<div>
|
||||
<h3 className="mb-2 font-bold">
|
||||
Writing Style: {currentStyleConfig.label}
|
||||
</h3>
|
||||
<p>
|
||||
Choose the writing style for your research reports. Different
|
||||
styles are optimized for different audiences and purposes.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
className="!border-brand !text-brand rounded-2xl"
|
||||
variant="outline"
|
||||
>
|
||||
<CurrentIcon className="h-4 w-4" /> {currentStyleConfig.label}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</Tooltip>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Choose Writing Style</DialogTitle>
|
||||
<DialogDescription>
|
||||
Select the writing style for your research reports. Each style is
|
||||
optimized for different audiences and purposes.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-3 py-4">
|
||||
{REPORT_STYLES.map((style) => {
|
||||
const Icon = style.icon;
|
||||
const isSelected = currentStyle === style.value;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={style.value}
|
||||
className={cn(
|
||||
"hover:bg-accent flex items-start gap-3 rounded-lg border p-4 text-left transition-colors",
|
||||
isSelected && "border-primary bg-accent",
|
||||
)}
|
||||
onClick={() => handleStyleChange(style.value)}
|
||||
>
|
||||
<Icon className="mt-0.5 h-5 w-5 shrink-0" />
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium">{style.label}</h4>
|
||||
{isSelected && <Check className="text-primary h-4 w-4" />}
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{style.description}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
import type { Resource } from "~/core/messages";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
export interface ResourceMentionsProps {
|
||||
items: Array<Resource>;
|
||||
command: (item: { id: string; label: string }) => void;
|
||||
}
|
||||
|
||||
export const ResourceMentions = forwardRef<
|
||||
{ onKeyDown: (args: { event: KeyboardEvent }) => boolean },
|
||||
ResourceMentionsProps
|
||||
>((props, ref) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const selectItem = (index: number) => {
|
||||
const item = props.items[index];
|
||||
|
||||
if (item) {
|
||||
props.command({ id: item.uri, label: item.title });
|
||||
}
|
||||
};
|
||||
|
||||
const upHandler = () => {
|
||||
setSelectedIndex(
|
||||
(selectedIndex + props.items.length - 1) % props.items.length,
|
||||
);
|
||||
};
|
||||
|
||||
const downHandler = () => {
|
||||
setSelectedIndex((selectedIndex + 1) % props.items.length);
|
||||
};
|
||||
|
||||
const enterHandler = () => {
|
||||
selectItem(selectedIndex);
|
||||
};
|
||||
|
||||
useEffect(() => setSelectedIndex(0), [props.items]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onKeyDown: ({ event }) => {
|
||||
if (event.key === "ArrowUp") {
|
||||
upHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
downHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === "Enter") {
|
||||
enterHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="bg-card border-var(--border) relative flex flex-col gap-1 overflow-auto rounded-md border p-2 shadow">
|
||||
{props.items.length ? (
|
||||
props.items.map((item, index) => (
|
||||
<button
|
||||
className={cn(
|
||||
"focus-visible:ring-ring hover:bg-accent hover:text-accent-foreground inline-flex h-9 w-full items-center justify-start gap-2 rounded-md px-4 py-2 text-sm whitespace-nowrap transition-colors focus-visible:ring-1 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
selectedIndex === index &&
|
||||
"bg-secondary text-secondary-foreground",
|
||||
)}
|
||||
key={index}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
{item.title}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="items-center justify-center text-gray-500">
|
||||
No result
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import type { MentionOptions } from "@tiptap/extension-mention";
|
||||
import { ReactRenderer } from "@tiptap/react";
|
||||
import {
|
||||
ResourceMentions,
|
||||
type ResourceMentionsProps,
|
||||
} from "./resource-mentions";
|
||||
import type { Instance, Props } from "tippy.js";
|
||||
import tippy from "tippy.js";
|
||||
import { resolveServiceURL } from "~/core/api/resolve-service-url";
|
||||
import type { Resource } from "~/core/messages";
|
||||
|
||||
export const resourceSuggestion: MentionOptions["suggestion"] = {
|
||||
items: ({ query }) => {
|
||||
return fetch(resolveServiceURL(`rag/resources?query=${query}`), {
|
||||
method: "GET",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
return res.resources as Array<Resource>;
|
||||
})
|
||||
.catch((err) => {
|
||||
return [];
|
||||
});
|
||||
},
|
||||
|
||||
render: () => {
|
||||
let reactRenderer: ReactRenderer<
|
||||
{ onKeyDown: (args: { event: KeyboardEvent }) => boolean },
|
||||
ResourceMentionsProps
|
||||
>;
|
||||
let popup: Instance<Props>[] | null = null;
|
||||
|
||||
return {
|
||||
onStart: (props) => {
|
||||
if (!props.clientRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
reactRenderer = new ReactRenderer(ResourceMentions, {
|
||||
props,
|
||||
editor: props.editor,
|
||||
});
|
||||
|
||||
popup = tippy("body", {
|
||||
getReferenceClientRect: props.clientRect as any,
|
||||
appendTo: () => document.body,
|
||||
content: reactRenderer.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: "manual",
|
||||
placement: "top-start",
|
||||
});
|
||||
},
|
||||
|
||||
onUpdate(props) {
|
||||
reactRenderer.updateProps(props);
|
||||
|
||||
if (!props.clientRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
popup?.[0]?.setProps({
|
||||
getReferenceClientRect: props.clientRect as any,
|
||||
});
|
||||
},
|
||||
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === "Escape") {
|
||||
popup?.[0]?.hide();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return reactRenderer.ref?.onKeyDown(props) ?? false;
|
||||
},
|
||||
|
||||
onExit() {
|
||||
popup?.[0]?.destroy();
|
||||
reactRenderer.destroy();
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,14 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { useEffect, useImperativeHandle, useRef, type ReactNode, type RefObject } from "react";
|
||||
import {
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
} from "react";
|
||||
import { useStickToBottom } from "use-stick-to-bottom";
|
||||
|
||||
import { ScrollArea } from "~/components/ui/scroll-area";
|
||||
@@ -26,15 +33,16 @@ export function ScrollContainer({
|
||||
scrollShadow = true,
|
||||
scrollShadowColor = "var(--background)",
|
||||
autoScrollToBottom = false,
|
||||
ref
|
||||
ref,
|
||||
}: ScrollContainerProps) {
|
||||
const { scrollRef, contentRef, scrollToBottom, isAtBottom } = useStickToBottom({ initial: "instant" });
|
||||
const { scrollRef, contentRef, scrollToBottom, isAtBottom } =
|
||||
useStickToBottom({ initial: "instant" });
|
||||
useImperativeHandle(ref, () => ({
|
||||
scrollToBottom() {
|
||||
if (isAtBottom) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const tempScrollRef = useRef<HTMLElement>(null);
|
||||
|
||||
@@ -66,17 +66,6 @@ const ReportEditor = ({ content, onMarkdownChange }: ReportEditorProps) => {
|
||||
|
||||
const debouncedUpdates = useDebouncedCallback(
|
||||
async (editor: EditorInstance) => {
|
||||
// const json = editor.getJSON();
|
||||
// // setCharsCount(editor.storage.characterCount.words());
|
||||
// window.localStorage.setItem(
|
||||
// "html-content",
|
||||
// highlightCodeblocks(editor.getHTML()),
|
||||
// );
|
||||
// window.localStorage.setItem("novel-content", JSON.stringify(json));
|
||||
// window.localStorage.setItem(
|
||||
// "markdown",
|
||||
// editor.storage.markdown.getMarkdown(),
|
||||
// );
|
||||
if (onMarkdownChange) {
|
||||
const markdown = editor.storage.markdown.getMarkdown();
|
||||
onMarkdownChange(markdown);
|
||||
@@ -86,12 +75,6 @@ const ReportEditor = ({ content, onMarkdownChange }: ReportEditorProps) => {
|
||||
500,
|
||||
);
|
||||
|
||||
// useEffect(() => {
|
||||
// const content = window.localStorage.getItem("novel-content");
|
||||
// if (content) setInitialContent(JSON.parse(content));
|
||||
// else setInitialContent(defaultEditorContent);
|
||||
// }, []);
|
||||
|
||||
if (!initialContent) return null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "~/lib/utils";
|
||||
import { motion, type MotionStyle, type Transition } from "motion/react";
|
||||
|
||||
interface BorderBeamProps {
|
||||
/**
|
||||
* The size of the border beam.
|
||||
*/
|
||||
size?: number;
|
||||
/**
|
||||
* The duration of the border beam.
|
||||
*/
|
||||
duration?: number;
|
||||
/**
|
||||
* The delay of the border beam.
|
||||
*/
|
||||
delay?: number;
|
||||
/**
|
||||
* The color of the border beam from.
|
||||
*/
|
||||
colorFrom?: string;
|
||||
/**
|
||||
* The color of the border beam to.
|
||||
*/
|
||||
colorTo?: string;
|
||||
/**
|
||||
* The motion transition of the border beam.
|
||||
*/
|
||||
transition?: Transition;
|
||||
/**
|
||||
* The class name of the border beam.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* The style of the border beam.
|
||||
*/
|
||||
style?: React.CSSProperties;
|
||||
/**
|
||||
* Whether to reverse the animation direction.
|
||||
*/
|
||||
reverse?: boolean;
|
||||
/**
|
||||
* The initial offset position (0-100).
|
||||
*/
|
||||
initialOffset?: number;
|
||||
}
|
||||
|
||||
export const BorderBeam = ({
|
||||
className,
|
||||
size = 50,
|
||||
delay = 0,
|
||||
duration = 6,
|
||||
colorFrom = "#ffaa40",
|
||||
colorTo = "#9c40ff",
|
||||
transition,
|
||||
style,
|
||||
reverse = false,
|
||||
initialOffset = 0,
|
||||
}: BorderBeamProps) => {
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 rounded-[inherit] border border-transparent [mask-image:linear-gradient(transparent,transparent),linear-gradient(#000,#000)] [mask-composite:intersect] [mask-clip:padding-box,border-box]">
|
||||
<motion.div
|
||||
className={cn(
|
||||
"absolute aspect-square",
|
||||
"bg-gradient-to-l from-[var(--color-from)] via-[var(--color-to)] to-transparent",
|
||||
className,
|
||||
)}
|
||||
style={
|
||||
{
|
||||
width: size,
|
||||
offsetPath: `rect(0 auto auto 0 round ${size}px)`,
|
||||
"--color-from": colorFrom,
|
||||
"--color-to": colorTo,
|
||||
...style,
|
||||
} as MotionStyle
|
||||
}
|
||||
initial={{ offsetDistance: `${initialOffset}%` }}
|
||||
animate={{
|
||||
offsetDistance: reverse
|
||||
? [`${100 - initialOffset}%`, `${-initialOffset}%`]
|
||||
: [`${initialOffset}%`, `${100 + initialOffset}%`],
|
||||
}}
|
||||
transition={{
|
||||
repeat: Infinity,
|
||||
ease: "linear",
|
||||
duration,
|
||||
delay: -delay,
|
||||
...transition,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@
|
||||
import { env } from "~/env";
|
||||
|
||||
import type { MCPServerMetadata } from "../mcp";
|
||||
import type { Resource } from "../messages";
|
||||
import { extractReplayIdFromSearchParams } from "../replay/get-replay-id";
|
||||
import { fetchStream } from "../sse";
|
||||
import { sleep } from "../utils";
|
||||
@@ -15,12 +16,15 @@ export async function* chatStream(
|
||||
userMessage: string,
|
||||
params: {
|
||||
thread_id: string;
|
||||
resources?: Array<Resource>;
|
||||
auto_accepted_plan: boolean;
|
||||
max_plan_iterations: number;
|
||||
max_step_num: number;
|
||||
max_search_results?: number;
|
||||
interrupt_feedback?: string;
|
||||
enable_deep_thinking?: boolean;
|
||||
enable_background_investigation: boolean;
|
||||
report_style?: "academic" | "popular_science" | "news" | "social_media";
|
||||
mcp_settings?: {
|
||||
servers: Record<
|
||||
string,
|
||||
@@ -101,7 +105,8 @@ async function* chatReplayStream(
|
||||
const text = await fetchReplay(replayFilePath, {
|
||||
abortSignal: options.abortSignal,
|
||||
});
|
||||
const chunks = text.split("\n\n");
|
||||
const normalizedText = text.replace(/\r\n/g, "\n");
|
||||
const chunks = normalizedText.split("\n\n");
|
||||
for (const chunk of chunks) {
|
||||
const [eventRaw, dataRaw] = chunk.split("\n") as [string, string];
|
||||
const [, event] = eventRaw.split("event: ", 2) as [string, string];
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { type DeerFlowConfig } from "../config/types";
|
||||
|
||||
import { resolveServiceURL } from "./resolve-service-url";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__deerflowConfig: DeerFlowConfig;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadConfig() {
|
||||
const res = await fetch(resolveServiceURL("./config"));
|
||||
const config = await res.json();
|
||||
return config;
|
||||
}
|
||||
|
||||
export function getConfig(): DeerFlowConfig {
|
||||
if (
|
||||
typeof window === "undefined" ||
|
||||
typeof window.__deerflowConfig === "undefined"
|
||||
) {
|
||||
throw new Error("Config not loaded");
|
||||
}
|
||||
return window.__deerflowConfig;
|
||||
}
|
||||
@@ -3,9 +3,12 @@
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { env } from "~/env";
|
||||
|
||||
import { useReplay } from "../replay";
|
||||
|
||||
import { fetchReplayTitle } from "./chat";
|
||||
import { getConfig } from "./config";
|
||||
|
||||
export function useReplayMetadata() {
|
||||
const { isReplay } = useReplay();
|
||||
@@ -39,3 +42,19 @@ export function useReplayMetadata() {
|
||||
}, [isLoading, isReplay, title]);
|
||||
return { title, isLoading, hasError: error };
|
||||
}
|
||||
|
||||
export function useRAGProvider() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [provider, setProvider] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setProvider(getConfig().rag.provider);
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
return { provider, loading };
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
export * from "./chat";
|
||||
export * from "./mcp";
|
||||
export * from "./podcast";
|
||||
export * from "./prompt-enhancer";
|
||||
export * from "./types";
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { resolveServiceURL } from "./resolve-service-url";
|
||||
|
||||
export interface EnhancePromptRequest {
|
||||
prompt: string;
|
||||
context?: string;
|
||||
report_style?: string;
|
||||
}
|
||||
|
||||
export interface EnhancePromptResponse {
|
||||
enhanced_prompt: string;
|
||||
}
|
||||
|
||||
export async function enhancePrompt(
|
||||
request: EnhancePromptRequest,
|
||||
): Promise<string> {
|
||||
const response = await fetch(resolveServiceURL("prompt/enhance"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Raw API response:", data); // Debug log
|
||||
|
||||
// The backend now returns the enhanced prompt directly in the result field
|
||||
let enhancedPrompt = data.result;
|
||||
|
||||
// If the result is somehow still a JSON object, extract the enhanced_prompt
|
||||
if (typeof enhancedPrompt === "object" && enhancedPrompt.enhanced_prompt) {
|
||||
enhancedPrompt = enhancedPrompt.enhanced_prompt;
|
||||
}
|
||||
|
||||
// If the result is a JSON string, try to parse it
|
||||
if (typeof enhancedPrompt === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(enhancedPrompt);
|
||||
if (parsed.enhanced_prompt) {
|
||||
enhancedPrompt = parsed.enhanced_prompt;
|
||||
}
|
||||
} catch {
|
||||
// If parsing fails, use the string as-is (which is what we want)
|
||||
console.log("Using enhanced prompt as-is:", enhancedPrompt);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to original prompt if something went wrong
|
||||
if (!enhancedPrompt || enhancedPrompt.trim() === "") {
|
||||
console.warn("No enhanced prompt received, using original");
|
||||
enhancedPrompt = request.prompt;
|
||||
}
|
||||
|
||||
return enhancedPrompt;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Resource } from "../messages";
|
||||
|
||||
import { resolveServiceURL } from "./resolve-service-url";
|
||||
|
||||
export function queryRAGResources(query: string) {
|
||||
return fetch(resolveServiceURL(`rag/resources?query=${query}`), {
|
||||
method: "GET",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
return res.resources as Array<Resource>;
|
||||
})
|
||||
.catch(() => {
|
||||
return [];
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user