├── .python-version ├── .gitattributes ├── video └── DeepSeek + 火山引擎搭建深度研究应用.mp4 ├── pyproject.toml ├── .env.example ├── __init__.py ├── search_engine ├── __init__.py ├── search_engine.py ├── volc_bot.py └── tavily.py ├── config.py ├── prompt.py ├── README.md ├── utils.py ├── server.py ├── .gitignore ├── webui.py └── deep_research.py /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | video/*.mp4 filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /video/DeepSeek + 火山引擎搭建深度研究应用.mp4: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:29faa311763d8c3cd84a3f8cb0fe86af35f01161f1659c7267638a86ef99cc86 3 | size 29541019 4 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "deepseek-deep-research" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [ 8 | "arkitect>=0.1.8", 9 | "gradio>=5.20.1", 10 | "openai>=1.65.5", 11 | "python-dotenv>=1.0.1", 12 | "tavily-python>=0.5.1", 13 | ] 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # 填写火山方舟API KEY 2 | ARK_API_KEY= 3 | 4 | # 思考模型名称,填写deepseek-r1-250120 5 | REASONING_MODEL=deepseek-r1-250120 6 | 7 | # 搜索引擎选择 (volc_bot 或 tavily) 8 | SEARCH_ENGINE=volc_bot 9 | 10 | # 如果使用火山方舟零代码联网应用作为搜索引擎,配置以下参数 11 | SEARCH_BOT_ID= 12 | 13 | # 如果使用tavily作为搜索引擎,配置以下参数 14 | TAVILY_API_KEY= 15 | 16 | # WebUI连接本地服务器配置 17 | API_ADDR=http://localhost:8888/api/v3/bots 18 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Bytedance Ltd. and/or its affiliates 2 | # Licensed under the 【火山方舟】原型应用软件自用许可协议 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # https://www.volcengine.com/docs/82379/1433703 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | -------------------------------------------------------------------------------- /search_engine/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Bytedance Ltd. and/or its affiliates 2 | # Licensed under the 【火山方舟】原型应用软件自用许可协议 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # https://www.volcengine.com/docs/82379/1433703 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | from .search_engine import SearchEngine, SearchResult, SearchReference 13 | 14 | __all__ = [ 15 | "SearchEngine", 16 | "SearchResult", 17 | "SearchReference" 18 | ] -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Bytedance Ltd. and/or its affiliates 2 | # Licensed under the 【火山方舟】原型应用软件自用许可协议 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # https://www.volcengine.com/docs/82379/1433703 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | import os 13 | 14 | """ 15 | for server 16 | """ 17 | 18 | # recommend to use DeepSeek-R1 model 19 | REASONING_MODEL = os.getenv('REASONING_MODEL') or "deepseek-r1-250120" 20 | # default set to volc bot, if using tavily, change it into "tavily" 21 | SEARCH_ENGINE = os.getenv('SEARCH_ENGINE') or "volc_bot" 22 | # optional, if you select tavily as search engine, please configure this 23 | TAVILY_API_KEY = os.getenv('TAVILY_API_KEY') or "{YOUR_TAVILY_API_KEY}" 24 | # optional, if you select volc bot as search engine, please configure this 25 | SEARCH_BOT_ID = os.getenv('SEARCH_BOT_ID') or "{YOUR_SEARCH_BOT_ID}" 26 | 27 | """ 28 | for webui 29 | """ 30 | 31 | # ark api key 32 | ARK_API_KEY = os.getenv('ARK_API_KEY') or "{YOUR_ARK_API_KEY}" 33 | # api server address for web ui 34 | API_ADDR = os.getenv("API_ADDR") or "https://ark.cn-beijing.volces.com/api/v3/bots" 35 | # while using remote api, need bot id 36 | API_BOT_ID = os.getenv("API_BOT_ID") or "{YOUR_API_BOT_ID}" 37 | -------------------------------------------------------------------------------- /search_engine/search_engine.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Bytedance Ltd. and/or its affiliates 2 | # Licensed under the 【火山方舟】原型应用软件自用许可协议 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # https://www.volcengine.com/docs/82379/1433703 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | from abc import ABC, abstractmethod 13 | from pydantic import BaseModel 14 | from typing import Optional, List 15 | 16 | """ 17 | single reference definition 18 | """ 19 | 20 | 21 | class SearchReference(BaseModel): 22 | site: Optional[str] 23 | title: Optional[str] 24 | url: Optional[str] 25 | content: Optional[str] 26 | 27 | 28 | """ 29 | search_result definition 30 | """ 31 | 32 | 33 | class SearchResult(BaseModel): 34 | # query is the input question 35 | query: str = "" 36 | # summary_content is a summary plain text of the query result. 37 | summary_content: Optional[str] = None 38 | # search_references is the raw references of searched result 39 | search_references: Optional[List[SearchReference]] = None 40 | 41 | 42 | """ 43 | search_engine interface 44 | """ 45 | 46 | 47 | class SearchEngine(BaseModel, ABC): 48 | 49 | @abstractmethod 50 | def search(self, queries: List[str]) -> List[SearchResult]: 51 | pass 52 | 53 | @abstractmethod 54 | async def asearch(self, queries: List[str]) -> List[SearchResult]: 55 | pass 56 | -------------------------------------------------------------------------------- /prompt.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Bytedance Ltd. and/or its affiliates 2 | # Licensed under the 【火山方舟】原型应用软件自用许可协议 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # https://www.volcengine.com/docs/82379/1433703 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | from jinja2 import Template 13 | 14 | DEFAULT_SUMMARY_PROMPT = Template( 15 | """# 历史对话 16 | {{chat_history}} 17 | 18 | # 联网参考资料 19 | {{reference}} 20 | 21 | # 当前环境信息 22 | {{meta_info}} 23 | 24 | # 任务 25 | - 优先参考「联网参考资料」中的信息进行回复。 26 | - 回复请使用清晰、结构化(序号/分段等)的语言,确保用户轻松理解和使用。 27 | - 如果回复内容中参考了「联网」中的信息,在请务必在正文的段落中引用对应的参考编号,例如[3][5] 28 | - 回答的最后需要列出已参考的所有资料信息。格式如下:[参考编号] 资料名称 29 | 示例: 30 | [1] 火山引擎 31 | [3] 火山方舟大模型服务平台 32 | 33 | # 任务执行 34 | 遵循任务要求来回答「用户问题」,给出有帮助的回答。 35 | 36 | 用户问题: 37 | {{question}} 38 | 39 | # 你的回答: 40 | """ 41 | ) 42 | 43 | DEFAULT_PLANNING_PROMPT = Template( 44 | """ 45 | 你是一个联网信息搜索专家,你需要根据用户的问题,通过联网搜索来搜集相关信息,然后根据这些信息来回答用户的问题。 46 | 47 | # 用户问题: 48 | {{question}} 49 | 50 | # 当前已知资料 51 | 52 | {{reference}} 53 | 54 | # 当前环境信息 55 | 56 | {{meta_info}} 57 | 58 | # 任务 59 | - 判断「当前已知资料」是否已经足够回答用户的问题 60 | - 如果「当前已知资料」已经足够回答用户的问题,返回“无需检索”,不要输出任何其他多余的内容 61 | - 如果判断「当前已知资料」还不足以回答用户的问题,思考还需要搜索什么信息,输出对应的关键词,请保证每个关键词的精简和独立性 62 | - 输出的每个关键词都应该要具体到可以用于独立检索,要包括完整的主语和宾语,避免歧义和使用代词,关键词之间不能有指代关系 63 | - 可以输出1 ~ {{max_search_words}}个关键词,当暂时无法提出足够准确的关键词时,请适当地减少关键词的数量 64 | - 输出多个关键词时,关键词之间用 ; 分割,不要输出其他任何多余的内容 65 | 66 | # 你的回答: 67 | """ 68 | ) 69 | -------------------------------------------------------------------------------- /search_engine/volc_bot.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Bytedance Ltd. and/or its affiliates 2 | # Licensed under the 【火山方舟】原型应用软件自用许可协议 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # https://www.volcengine.com/docs/82379/1433703 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | import asyncio 13 | from abc import ABC 14 | from typing import List, Optional 15 | 16 | from volcenginesdkarkruntime import AsyncArk 17 | from volcenginesdkarkruntime.types.bot_chat import BotChatCompletion 18 | 19 | from .search_engine import SearchEngine, SearchResult, SearchReference 20 | 21 | """ 22 | using volc bot (with search plugin) to search 23 | """ 24 | 25 | 26 | class VolcBotSearchEngine(SearchEngine, ABC): 27 | 28 | def __init__( 29 | self, 30 | bot_id: str, 31 | api_key: Optional[str] = None, 32 | ): 33 | super().__init__() 34 | self._bot_id = bot_id 35 | self._ark_client = AsyncArk(api_key=api_key) 36 | 37 | def search(self, queries: List[str]) -> List[SearchResult]: 38 | return asyncio.run(self.asearch(queries=queries)) 39 | 40 | async def asearch(self, queries: List[str]) -> List[SearchResult]: 41 | tasks = [self._single_search(query) for query in queries] 42 | task_results = await asyncio.gather(*tasks) 43 | return [ 44 | result for result in task_results 45 | ] 46 | 47 | async def _single_search(self, query: str) -> SearchResult: 48 | response = await self._run_bot_search(query) 49 | return self._format_result(response, query) 50 | 51 | async def _run_bot_search(self, query: str) -> BotChatCompletion: 52 | return await self._ark_client.bot_chat.completions.create( 53 | messages=[ 54 | { 55 | "role": "user", 56 | "content": query, 57 | } 58 | ], 59 | model=self._bot_id, 60 | stream=False, 61 | ) 62 | 63 | @classmethod 64 | def _format_result(cls, response: BotChatCompletion, query: str) -> SearchResult: 65 | return SearchResult( 66 | query=query, 67 | summary_content=response.choices[0].message.content, 68 | search_references=[ 69 | SearchReference( 70 | site=r.site_name, 71 | url=r.url, 72 | content=r.summary, 73 | title=r.title, 74 | ) for r in response.references if response.references 75 | ] 76 | ) 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 深度研究 Deep Research 2 | 3 | ## 应用介绍 4 | 5 | Deep Research 是一款专为应对复杂问题而设计的高效工具,利用 DeepSeek-R1 大模型对复杂问题进行多角度分析,并辅助互联网资料,快速生成最合适用户的解决方案。 6 | 无论是在学术研究、企业决策还是产品调研中,Deep Research 都能够有效地协助用户深入挖掘,提出切实可行的解决策略。 7 | 8 | 该项目引用自[火山引擎高代码Python SDK Arkitect的示例](https://github.com/volcengine/ai-app-lab/tree/main/demohouse/deep_research),对其运行开发环境及运行环境做了微小的调整。 9 | 10 | 请参考[火山引擎高代码Python SDK Arkitect](https://github.com/volcengine/ai-app-lab)了解更多技术细节。 11 | 12 | ## 视频演示 (Video Demonstration) 13 | 14 | 本仓库包含一个视频演示,展示了如何使用 DeepSeek 和火山引擎搭建深度研究应用。您可以通过以下方式观看视频: 15 | 16 | 1. 直接在 GitHub 上查看:[DeepSeek + 火山引擎搭建深度研究应用.mp4](video/DeepSeek%20%2B%20%E7%81%AB%E5%B1%B1%E5%BC%95%E6%93%8E%E6%90%AD%E5%BB%BA%E6%B7%B1%E5%BA%A6%E7%A0%94%E7%A9%B6%E5%BA%94%E7%94%A8.mp4) 17 | 18 | 2. 克隆仓库后在本地观看: 19 | ```shell 20 | $ git clone https://github.com/sugarforever/deepseek-deep-research.git 21 | $ cd deepseek-deep-research 22 | # 使用您喜欢的视频播放器打开视频文件 23 | $ open "video/DeepSeek + 火山引擎搭建深度研究应用.mp4" # macOS 24 | # 或者 25 | $ xdg-open "video/DeepSeek + 火山引擎搭建深度研究应用.mp4" # Linux 26 | # 或者 27 | $ start "video/DeepSeek + 火山引擎搭建深度研究应用.mp4" # Windows 28 | ``` 29 | 30 | ## 环境准备 31 | 32 | - uv 包管理工具,可参考[安装文档](https://docs.astral.sh/uv/) 33 | 34 | - 获取火山方舟 API KEY | 参考文档 35 | - 在开通管理页开通 DeepSeek-R1 模型。 36 | - 搜索引擎选择:以下方式任选其一 37 | - 使用火山方舟零代码联网应用作为搜索引擎,推荐配置参见【附录】,操作步骤详情见 参考文档 38 | - 使用开源搜索引擎 Tavily,需获取 Tavily APIKEY 参考文档 39 | 40 | ## 安装运行 41 | 42 | 1. 下载代码库 43 | 44 | ```shell 45 | $ git clone https://github.com/sugarforever/deepseek-deep-research.git 46 | $ cd deepseek-deep-research 47 | $ uv sync 48 | ``` 49 | 50 | 2. 配置环境变量 51 | 52 | 复制 `.env.example` 文件为 `.env` 并根据您选择的搜索引擎填写相应的配置项: 53 | 54 | ```shell 55 | cp .env.example .env 56 | # 使用您喜欢的编辑器编辑 .env 文件 57 | ``` 58 | 59 | `.env` 文件中包含了不同搜索引擎所需的配置项,请根据您的选择填写相应的参数。 60 | 61 | 3. 运行服务 62 | 63 | ```shell 64 | # 启动API服务 65 | uv run server.py 66 | 67 | # 在另一个终端窗口启动WebUI 68 | uv run webui.py 69 | ``` 70 | 71 | 4. 使用浏览器访问 `http://localhost:7860/` 即可使用 72 | 73 | 74 | ## 技术实现 75 | 76 | 本项目结合深度思考大模型和联网搜索能力,并向上封装成标准的 Chat Completion API Server。 77 | 78 | 在接收到用户的原始问题后,会进行两个阶段的处理: 79 | 80 | 1. **思考阶段(循环进行)** 81 | 82 | DeepSeek-R1 根据用户问题不断地使用搜索引擎,获取参考资料,直至模型认为收集到的参考资料已经足够解决用户问题。 83 | 84 | 85 | 2. **总结阶段** 86 | 87 | DeepSeek-R1 根据上一阶段产出的所有参考资料和思考过程中的模型输出,对用户的问题进行总结性回答。 88 | 89 | 其中,思考阶段的模型输出会整合至`reasoning_content`字段中,总结阶段的模型输出会整合至`content`字段中。该架构严格遵循 90 | OpenAI Chat Completion API 规范设计,因此开发者可直接使用 OpenAI 标准 SDK 或兼容接口实现服务的无缝对接,显著降低了技术集成的复杂度。 -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Bytedance Ltd. and/or its affiliates 2 | # Licensed under the 【火山方舟】原型应用软件自用许可协议 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # https://www.volcengine.com/docs/82379/1433703 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | import asyncio 12 | import time 13 | from datetime import datetime 14 | from typing import List, AsyncIterable, Generator 15 | 16 | import volcenginesdkarkruntime.types.chat.chat_completion_chunk as completion_chunk 17 | 18 | from arkitect.core.component.llm.model import ArkMessage, ArkChatCompletionChunk 19 | 20 | 21 | def cast_content_to_reasoning_content( 22 | chunk: ArkChatCompletionChunk, 23 | ) -> ArkChatCompletionChunk: 24 | new_chunk = ArkChatCompletionChunk(**chunk.__dict__) 25 | new_chunk.choices[0].delta.reasoning_content = chunk.choices[0].delta.content 26 | new_chunk.choices[0].delta.content = "" 27 | return new_chunk 28 | 29 | 30 | def cast_reference_to_chunks(keyword: str, raw_content: str) -> ArkChatCompletionChunk: 31 | new_chunk = ArkChatCompletionChunk( 32 | id="", 33 | object="chat.completion.chunk", 34 | created=0, 35 | model="", 36 | choices=[], 37 | metadata={ 38 | "reference": raw_content, 39 | "keyword": keyword, 40 | }, 41 | ) 42 | return new_chunk 43 | 44 | 45 | def get_last_message(messages: List[ArkMessage], role: str): 46 | """Finds the last ArkMessage of a specific role, given the role.""" 47 | for message in reversed(messages): 48 | if message.role == role: 49 | return message 50 | return None 51 | 52 | 53 | def get_current_date() -> str: 54 | return datetime.now().strftime("%Y年%m月%d日") 55 | 56 | 57 | def gen_metadata_chunk(metadata: dict) -> ArkChatCompletionChunk: 58 | return ArkChatCompletionChunk( 59 | id='', 60 | created=int(time.time()), 61 | model='', 62 | object='chat.completion.chunk', 63 | choices=[completion_chunk.Choice( 64 | index=0, 65 | delta=completion_chunk.ChoiceDelta( 66 | content="", 67 | reasoning_content="" 68 | ), 69 | )], 70 | metadata=metadata, 71 | ) 72 | 73 | 74 | def sync_wrapper(async_generator: AsyncIterable) -> Generator: 75 | loop = asyncio.new_event_loop() 76 | try: 77 | gen = async_generator 78 | _aiter = gen.__aiter__() 79 | while True: 80 | try: 81 | item = loop.run_until_complete(_aiter.__anext__()) 82 | yield item 83 | except StopAsyncIteration: 84 | break 85 | finally: 86 | loop.close() 87 | -------------------------------------------------------------------------------- /search_engine/tavily.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Bytedance Ltd. and/or its affiliates 2 | # Licensed under the 【火山方舟】原型应用软件自用许可协议 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # https://www.volcengine.com/docs/82379/1433703 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | from abc import ABC 13 | from typing import Literal, Optional, List 14 | 15 | from tavily import TavilyClient 16 | 17 | from .search_engine import SearchEngine, SearchResult 18 | 19 | import asyncio 20 | 21 | 22 | class TavilySearchEngine(SearchEngine, ABC): 23 | 24 | def __init__( 25 | self, 26 | api_key: str, 27 | search_depth: Literal["basic", "advanced"] = "basic", 28 | topic: Literal["general", "news"] = "general", 29 | days: int = 3, 30 | max_results: int = 5, 31 | include_domains: Optional[str] = None, 32 | exclude_domains: Optional[str] = None, 33 | ): 34 | super().__init__() 35 | self._tavily_client = TavilyClient(api_key=api_key) 36 | self._search_depth = search_depth 37 | self._topic = topic 38 | self._days = days 39 | self._max_results = max_results 40 | self._include_domains = include_domains 41 | self._exclude_domains = exclude_domains 42 | 43 | def search(self, queries: List[str]) -> List[SearchResult]: 44 | return asyncio.run(self.asearch(queries=queries)) 45 | 46 | async def asearch(self, queries: List[str]) -> List[SearchResult]: 47 | tasks = [self._arun_search_single(query) for query in queries] 48 | task_results = await asyncio.gather(*tasks) 49 | return [ 50 | r for r in task_results 51 | ] 52 | 53 | async def _arun_search_single(self, query: str) -> SearchResult: 54 | return await asyncio.to_thread(self._search_single, query) 55 | 56 | def _search_single(self, query: str) -> SearchResult: 57 | response = self._tavily_client.search( 58 | query=query, 59 | search_depth=self._search_depth, 60 | topic=self._topic, 61 | days=self._days, 62 | max_results=self._max_results, 63 | include_domains=self._include_domains, 64 | exclude_domains=self._exclude_domains, 65 | ) 66 | return SearchResult( 67 | query=query, 68 | summary_content=self._format_result(response), 69 | ) 70 | 71 | @classmethod 72 | def _format_result(cls, tavily_result: dict) -> str: 73 | results = tavily_result.get("results", []) 74 | formatted: str = "" 75 | for (i, result) in enumerate(results): 76 | formatted += f"参考资料{i + 1}: \n" 77 | formatted += f"标题: {result.get('title', '')}\n" 78 | formatted += f"内容: {result.get('content', '')}\n" 79 | formatted += "\n" 80 | return formatted 81 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Bytedance Ltd. and/or its affiliates 2 | # Licensed under the 【火山方舟】原型应用软件自用许可协议 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # https://www.volcengine.com/docs/82379/1433703 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | import logging 13 | import os 14 | from typing import AsyncIterable, Union 15 | 16 | from dotenv import load_dotenv 17 | 18 | load_dotenv() 19 | 20 | from arkitect.core.component.llm.model import ( 21 | ArkChatCompletionChunk, 22 | ArkChatRequest, 23 | ArkChatResponse, 24 | ) 25 | from arkitect.launcher.local.serve import launch_serve 26 | from arkitect.launcher.vefaas import bot_wrapper 27 | from arkitect.telemetry.trace import task 28 | from search_engine.tavily import TavilySearchEngine 29 | from search_engine.volc_bot import VolcBotSearchEngine 30 | from deep_research import DeepResearch, ExtraConfig 31 | 32 | from utils import get_last_message 33 | 34 | from config import ( 35 | REASONING_MODEL, 36 | SEARCH_ENGINE, 37 | TAVILY_API_KEY, 38 | SEARCH_BOT_ID, 39 | ) 40 | 41 | logging.basicConfig( 42 | level=logging.INFO, format="[%(asctime)s][%(levelname)s] %(message)s" 43 | ) 44 | LOGGER = logging.getLogger(__name__) 45 | 46 | 47 | @task() 48 | async def main( 49 | request: ArkChatRequest, 50 | ) -> AsyncIterable[Union[ArkChatCompletionChunk, ArkChatResponse]]: 51 | # using last_user_message as query 52 | last_user_message = get_last_message(request.messages, "user") 53 | # set search_engine 54 | search_engine = VolcBotSearchEngine(bot_id=SEARCH_BOT_ID) 55 | if "tavily" == SEARCH_ENGINE: 56 | search_engine = TavilySearchEngine(api_key=TAVILY_API_KEY) 57 | 58 | # settings from request 59 | metadata = request.metadata or {} 60 | max_search_words = metadata.get('max_search_words', 5) 61 | max_planning_rounds = metadata.get('max_planning_rounds', 5) 62 | 63 | deep_research = DeepResearch( 64 | search_engine=search_engine, 65 | planning_endpoint_id=REASONING_MODEL, 66 | summary_endpoint_id=REASONING_MODEL, 67 | extra_config=ExtraConfig( 68 | # optional, the max search words for each planning rounds 69 | max_search_words=max_search_words, 70 | # optional, the max rounds to run planning 71 | max_planning_rounds=max_planning_rounds, 72 | ) 73 | ) 74 | 75 | if request.stream: 76 | async for c in deep_research.astream_deep_research(request=request, question=last_user_message.content): 77 | yield c 78 | else: 79 | rsp = await deep_research.arun_deep_research(request=request, question=last_user_message.content) 80 | yield rsp 81 | 82 | 83 | @bot_wrapper() 84 | @task(custom_attributes={"input": None, "output": None}) 85 | async def handler( 86 | request: ArkChatRequest, 87 | ) -> AsyncIterable[Union[ArkChatCompletionChunk, ArkChatResponse]]: 88 | async for resp in main(request): 89 | yield resp 90 | 91 | 92 | if __name__ == "__main__": 93 | port = os.getenv("_FAAS_RUNTIME_PORT") 94 | launch_serve( 95 | package_path="server", 96 | port=int(port) if port else 8888, 97 | health_check_path="/v1/ping", 98 | endpoint_path="/api/v3/bots/chat/completions", 99 | trace_on=False, 100 | clients={}, 101 | ) 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | -------------------------------------------------------------------------------- /webui.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Bytedance Ltd. and/or its affiliates 2 | # Licensed under the 【火山方舟】原型应用软件自用许可协议 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # https://www.volcengine.com/docs/82379/1433703 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | import asyncio 13 | import logging 14 | import gradio as gr 15 | from gradio import ChatMessage 16 | from pydantic import BaseModel 17 | from openai import OpenAI 18 | from typing import Any, Generator 19 | from dotenv import load_dotenv 20 | load_dotenv() 21 | 22 | from config import (ARK_API_KEY, API_ADDR, API_BOT_ID) 23 | 24 | client = OpenAI(base_url=API_ADDR, api_key=ARK_API_KEY) 25 | 26 | logging.basicConfig( 27 | level=logging.INFO, format="[%(asctime)s][%(levelname)s] %(message)s" 28 | ) 29 | LOGGER = logging.getLogger(__name__) 30 | 31 | # store the search records 32 | search_records = [] 33 | 34 | global_css = """ 35 | .search-panel { 36 | height: 800px; 37 | overflow: auto; 38 | border: 1px solid #ccc; 39 | padding: 10px; 40 | } 41 | 42 | .chat-col { 43 | height: 100%; 44 | } 45 | """ 46 | 47 | 48 | def update_search_panel(): 49 | """生成右侧搜索面板的HTML内容""" 50 | html = "