mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-02 22:02:13 +08:00
feat: RAG Integration (#238)
* feat: add rag provider and retriever * feat: retriever tool * feat: add retriever tool to the researcher node * feat: add rag http apis * feat: new message input supports resource mentions * feat: new message input component support resource mentions * refactor: need_web_search to need_search * chore: RAG integration docs * chore: change example api host * fix: user message color in dark mode * fix: mentions style * feat: add local_search_tool to researcher prompt * chore: research prompt * fix: ragflow page size and reporter with * docs: ragflow integration and add acknowledgment projects * chore: format
This commit is contained in:
@@ -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
|
||||
|
||||
15
README.md
15
README.md
@@ -189,6 +189,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 +364,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 +551,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.
|
||||
|
||||
|
||||
@@ -2,16 +2,21 @@
|
||||
# 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
|
||||
|
||||
|
||||
@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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -17,6 +17,7 @@ from src.tools.search import LoggedTavilySearch
|
||||
from src.tools import (
|
||||
crawl_tool,
|
||||
get_web_search_tool,
|
||||
get_retriever_tool,
|
||||
python_repl_tool,
|
||||
)
|
||||
|
||||
@@ -206,10 +207,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"])
|
||||
@@ -242,7 +244,7 @@ def coordinator_node(
|
||||
logger.debug(f"Coordinator response: {response}")
|
||||
|
||||
return Command(
|
||||
update={"locale": locale},
|
||||
update={"locale": locale, "resources": configurable.resources},
|
||||
goto=goto,
|
||||
)
|
||||
|
||||
@@ -326,14 +328,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 +349,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 +392,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 +484,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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
@@ -15,6 +13,7 @@ class State(MessagesState):
|
||||
# Runtime Variables
|
||||
locale: str = "en-US"
|
||||
observations: list[str] = []
|
||||
resources: list[Resource] = []
|
||||
plan_iterations: int = 0
|
||||
current_plan: Plan | str = None
|
||||
final_report: str = ""
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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.
|
||||
|
||||
5
src/rag/__init__.py
Normal file
5
src/rag/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .retriever import Retriever, Document, Resource
|
||||
from .ragflow import RAGFlowProvider
|
||||
from .builder import build_retriever
|
||||
|
||||
__all__ = [Retriever, Document, Resource, RAGFlowProvider, build_retriever]
|
||||
11
src/rag/builder.py
Normal file
11
src/rag/builder.py
Normal file
@@ -0,0 +1,11 @@
|
||||
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
|
||||
130
src/rag/ragflow.py
Normal file
130
src/rag/ragflow.py
Normal file
@@ -0,0 +1,130 @@
|
||||
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)
|
||||
77
src/rag/retriever.py
Normal file
77
src/rag/retriever.py
Normal file
@@ -0,0 +1,77 @@
|
||||
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
|
||||
@@ -5,19 +5,22 @@ 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.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.rag.builder import build_retriever
|
||||
from src.rag.retriever import Resource
|
||||
from src.server.chat_request import (
|
||||
ChatMessage,
|
||||
ChatRequest,
|
||||
@@ -28,6 +31,11 @@ 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.tools import VolcengineTTS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -59,6 +67,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,
|
||||
@@ -74,6 +83,7 @@ async def chat_stream(request: ChatRequest):
|
||||
async def _astream_workflow_generator(
|
||||
messages: List[ChatMessage],
|
||||
thread_id: str,
|
||||
resources: List[Resource],
|
||||
max_plan_iterations: int,
|
||||
max_step_num: int,
|
||||
max_search_results: int,
|
||||
@@ -101,6 +111,7 @@ 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,
|
||||
@@ -319,3 +330,18 @@ async def mcp_server_metadata(request: MCPServerMetadataRequest):
|
||||
logger.exception(f"Error in MCP server metadata endpoint: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
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=[])
|
||||
|
||||
@@ -5,6 +5,8 @@ from typing import List, Optional, Union
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from src.rag.retriever import Resource
|
||||
|
||||
|
||||
class ContentItem(BaseModel):
|
||||
type: str = Field(..., description="The type of content (text, image, etc.)")
|
||||
@@ -28,6 +30,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"
|
||||
|
||||
25
src/server/rag_request.py
Normal file
25
src/server/rag_request.py
Normal file
@@ -0,0 +1,25 @@
|
||||
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",
|
||||
]
|
||||
|
||||
74
src/tools/retriever.py
Normal file
74
src/tools/retriever.py
Normal file
@@ -0,0 +1,74 @@
|
||||
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("三打白骨精"))
|
||||
@@ -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))
|
||||
|
||||
@@ -14,8 +14,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 +90,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,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"check": "next lint && tsc --noEmit",
|
||||
"dev": "next dev --turbo",
|
||||
"dev": "dotenv -f ../.env -e true run 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",
|
||||
|
||||
210
web/pnpm-lock.yaml
generated
210
web/pnpm-lock.yaml
generated
@@ -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))
|
||||
@@ -1193,6 +1208,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 +1464,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 +1535,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 +1600,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
|
||||
|
||||
@@ -3520,6 +3592,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 +3729,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 +5140,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 +5377,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 +5437,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 +5490,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 +5543,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 +5555,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
|
||||
|
||||
@@ -7816,6 +7983,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 +8212,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: {}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -3,18 +3,15 @@
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ArrowUp, X } from "lucide-react";
|
||||
import {
|
||||
type KeyboardEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useCallback, useRef } from "react";
|
||||
|
||||
import { Detective } from "~/components/deer-flow/icons/detective";
|
||||
import MessageInput, {
|
||||
type MessageInputRef,
|
||||
} from "~/components/deer-flow/message-input";
|
||||
import { Tooltip } from "~/components/deer-flow/tooltip";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import type { Option } from "~/core/messages";
|
||||
import type { Option, Resource } from "~/core/messages";
|
||||
import {
|
||||
setEnableBackgroundInvestigation,
|
||||
useSettingsStore,
|
||||
@@ -23,7 +20,6 @@ import { cn } from "~/lib/utils";
|
||||
|
||||
export function InputBox({
|
||||
className,
|
||||
size,
|
||||
responding,
|
||||
feedback,
|
||||
onSend,
|
||||
@@ -34,72 +30,52 @@ 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 backgroundInvestigation = useSettingsStore(
|
||||
(state) => state.general.enableBackgroundInvestigation,
|
||||
);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<MessageInputRef>(null);
|
||||
const feedbackRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (feedback) {
|
||||
setMessage("");
|
||||
|
||||
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>) => {
|
||||
console.log(message, resources);
|
||||
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?.();
|
||||
}
|
||||
}
|
||||
},
|
||||
[responding, imeStatus, handleSendMessage],
|
||||
[responding, onCancel, onSend, feedback, onRemoveFeedback],
|
||||
);
|
||||
|
||||
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 && (
|
||||
@@ -122,25 +98,10 @@ 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",
|
||||
)}
|
||||
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);
|
||||
}}
|
||||
<MessageInput
|
||||
className={cn("h-24 px-4 pt-3")}
|
||||
ref={inputRef}
|
||||
onEnter={handleSendMessage}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center px-4 py-2">
|
||||
@@ -181,7 +142,7 @@ export function InputBox({
|
||||
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">
|
||||
|
||||
@@ -174,7 +174,14 @@ function MessageListItem({
|
||||
>
|
||||
<MessageBubble message={message}>
|
||||
<div className="flex w-full flex-col">
|
||||
<Markdown>{message?.content}</Markdown>
|
||||
<Markdown
|
||||
className={cn(
|
||||
message.role === "user" &&
|
||||
"prose-invert not-dark:text-secondary dark:text-inherit",
|
||||
)}
|
||||
>
|
||||
{message?.content}
|
||||
</Markdown>
|
||||
</div>
|
||||
</MessageBubble>
|
||||
</div>
|
||||
@@ -214,9 +221,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,
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -276,8 +276,8 @@ function CrawlToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
}
|
||||
|
||||
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 +302,7 @@ function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
boxShadow: "none",
|
||||
}}
|
||||
>
|
||||
{code.trim()}
|
||||
{code?.trim() ?? ""}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
184
web/src/components/deer-flow/message-input.tsx
Normal file
184
web/src/components/deer-flow/message-input.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
// 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 { cx } from "class-variance-authority";
|
||||
|
||||
import "~/styles/prosemirror.css";
|
||||
import { resourceSuggestion } from "./resource-suggestion";
|
||||
import React, { forwardRef, useMemo, useRef } from "react";
|
||||
import type { Resource } from "~/core/messages";
|
||||
import { useRAGProvider } from "~/core/api/hooks";
|
||||
|
||||
export interface MessageInputRef {
|
||||
focus: () => void;
|
||||
submit: () => 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 debouncedUpdates = useDebouncedCallback(
|
||||
async (editor: EditorInstance) => {
|
||||
if (onChange) {
|
||||
const markdown = editor.storage.markdown.getMarkdown();
|
||||
onChange(markdown);
|
||||
}
|
||||
},
|
||||
200,
|
||||
);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
editorRef.current?.view.focus();
|
||||
},
|
||||
submit: () => {
|
||||
if (onEnter) {
|
||||
const { text, resources } = formatMessage(
|
||||
editorRef.current?.getJSON() ?? [],
|
||||
);
|
||||
onEnter(text, resources);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
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({
|
||||
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 (onEnter) {
|
||||
const { text, resources } = formatMessage(
|
||||
this.editor.getJSON() ?? [],
|
||||
);
|
||||
onEnter(text, resources);
|
||||
}
|
||||
return this.editor.commands.clearContent();
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
];
|
||||
if (provider) {
|
||||
extensions.push(
|
||||
Mention.configure({
|
||||
HTMLAttributes: {
|
||||
class: "mention",
|
||||
},
|
||||
suggestion: resourceSuggestion,
|
||||
}) as Extension,
|
||||
);
|
||||
}
|
||||
return extensions;
|
||||
}, [onEnter, provider]);
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 prose-headings:font-title font-default focus:outline-none max-w-full",
|
||||
},
|
||||
}}
|
||||
onCreate={({ editor }) => {
|
||||
editorRef.current = editor;
|
||||
}}
|
||||
onUpdate={({ editor }) => {
|
||||
debouncedUpdates(editor);
|
||||
}}
|
||||
></EditorContent>
|
||||
</EditorRoot>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default MessageInput;
|
||||
84
web/src/components/deer-flow/resource-mentions.tsx
Normal file
84
web/src/components/deer-flow/resource-mentions.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
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>
|
||||
);
|
||||
});
|
||||
83
web/src/components/deer-flow/resource-suggestion.tsx
Normal file
83
web/src/components/deer-flow/resource-suggestion.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
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();
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,6 +16,7 @@ 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;
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { env } from "~/env";
|
||||
|
||||
import { useReplay } from "../replay";
|
||||
|
||||
import { fetchReplayTitle } from "./chat";
|
||||
import { getRAGConfig } from "./rag";
|
||||
|
||||
export function useReplayMetadata() {
|
||||
const { isReplay } = useReplay();
|
||||
@@ -39,3 +42,26 @@ 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;
|
||||
}
|
||||
getRAGConfig()
|
||||
.then(setProvider)
|
||||
.catch((e) => {
|
||||
setProvider(null);
|
||||
console.error("Failed to get RAG provider", e);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { provider, loading };
|
||||
}
|
||||
|
||||
24
web/src/core/api/rag.ts
Normal file
24
web/src/core/api/rag.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
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((err) => {
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
export function getRAGConfig() {
|
||||
return fetch(resolveServiceURL(`rag/config`), {
|
||||
method: "GET",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => res.provider);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ export interface Message {
|
||||
options?: Option[];
|
||||
finishReason?: "stop" | "interrupt" | "tool_calls";
|
||||
interruptFeedback?: string;
|
||||
resources?: Array<Resource>;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
@@ -35,3 +36,8 @@ export interface ToolCallRuntime {
|
||||
argsChunks?: string[];
|
||||
result?: string;
|
||||
}
|
||||
|
||||
export interface Resource {
|
||||
uri: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { create } from "zustand";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
import { chatStream, generatePodcast } from "../api";
|
||||
import type { Message } from "../messages";
|
||||
import type { Message, Resource } from "../messages";
|
||||
import { mergeMessage } from "../messages";
|
||||
import { parseJSON } from "../utils";
|
||||
|
||||
@@ -78,8 +78,10 @@ export async function sendMessage(
|
||||
content?: string,
|
||||
{
|
||||
interruptFeedback,
|
||||
resources,
|
||||
}: {
|
||||
interruptFeedback?: string;
|
||||
resources?: Array<Resource>;
|
||||
} = {},
|
||||
options: { abortSignal?: AbortSignal } = {},
|
||||
) {
|
||||
@@ -90,6 +92,7 @@ export async function sendMessage(
|
||||
role: "user",
|
||||
content: content,
|
||||
contentChunks: [content],
|
||||
resources,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,6 +102,7 @@ export async function sendMessage(
|
||||
{
|
||||
thread_id: THREAD_ID,
|
||||
interrupt_feedback: interruptFeedback,
|
||||
resources,
|
||||
auto_accepted_plan: settings.autoAcceptedPlan,
|
||||
enable_background_investigation:
|
||||
settings.enableBackgroundInvestigation ?? true,
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
--brand: #5494f3;
|
||||
--brand: rgb(17, 103, 234);
|
||||
|
||||
--novel-highlight-default: #000000;
|
||||
--novel-highlight-purple: #3f2c4b;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
@import "./globals.css";
|
||||
|
||||
.prose {
|
||||
color: inherit;
|
||||
max-width: inherit;
|
||||
}
|
||||
|
||||
.prose .placeholder {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
@@ -15,6 +17,7 @@
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.ProseMirror p.is-empty::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
@@ -23,6 +26,14 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.ProseMirror .mention {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
box-decoration-break: clone;
|
||||
color: var(--brand);
|
||||
padding: 0.1rem 0.3rem;
|
||||
}
|
||||
|
||||
/* Custom image styles */
|
||||
|
||||
.ProseMirror img {
|
||||
|
||||
Reference in New Issue
Block a user