Merge branch 'dev' into staging

This commit is contained in:
朱潮 2026-04-29 17:28:59 +08:00
commit 100007f66b
25 changed files with 2053 additions and 76 deletions

View File

@ -1,5 +1,5 @@
# 使用Python 3.12官方镜像作为基础镜像
FROM python:3.12-slim
FROM python:3.12-slim AS base
# 设置工作目录
WORKDIR /app
@ -43,17 +43,27 @@ RUN pip install --no-cache-dir playwright && \
RUN npm install -g playwright sharp nodemailer dotenv && \
npx playwright install chromium
# 复制应用代码
COPY . .
# 创建必要的目录
RUN mkdir -p /app/projects
RUN mkdir -p /app/projects
RUN mkdir -p /app/public
RUN mkdir -p /app/models
# 下载sentence-transformers模型到models目录
RUN python -c "from sentence_transformers import SentenceTransformer; model = SentenceTransformer('TaylorAI/gte-tiny'); model.save('/app/models/gte-tiny')"
FROM base AS bytecode-builder
# 复制应用代码,仅在构建阶段编译为字节码
COPY . .
RUN python -m compileall -b /app && \
find /app -type f -name '*.py' -delete && \
find /app -type d -name '__pycache__' -prune -exec rm -rf {} +
FROM base AS runtime
# 只复制编译后的应用文件,避免源码进入最终镜像层
COPY --from=bytecode-builder /app /app
# 暴露端口
EXPOSE 8001
@ -62,4 +72,4 @@ HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8001/api/health || exit 1
# 启动命令 - profile通过环境变量PROFILE配置默认为balanced
CMD ["sh", "-c", "python3 start_unified.py --profile ${PROFILE:-balanced}"]
CMD ["sh", "-c", "python3 start_unified.pyc --profile ${PROFILE:-balanced}"]

View File

@ -1,5 +1,5 @@
# 使用Python 3.12官方镜像作为基础镜像
FROM docker.1ms.run/python:3.12-slim
FROM docker.1ms.run/python:3.12-slim AS base
# 设置工作目录
WORKDIR /app
@ -52,11 +52,18 @@ RUN mkdir -p /app/projects
RUN mkdir -p /app/public
RUN mkdir -p /app/models
# 从modelscope下载sentence-transformers模型到models目录
#RUN python -c "from modelscope import snapshot_download; model_dir = snapshot_download('TaylorAI/gte-tiny'); import shutil; shutil.move(model_dir, '/app/models/gte-tiny')"
FROM base AS bytecode-builder
# 复制应用代码
# 复制应用代码,仅在构建阶段编译为字节码
COPY . .
RUN python -m compileall -b /app && \
find /app -type f -name '*.py' -delete && \
find /app -type d -name '__pycache__' -prune -exec rm -rf {} +
FROM base AS runtime
# 只复制编译后的应用文件,避免源码进入最终镜像层
COPY --from=bytecode-builder /app /app
# 暴露端口
EXPOSE 8001
@ -66,4 +73,4 @@ HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8001/api/health || exit 1
# 启动命令 - 使用优化的统一启动脚本
CMD ["sh", "-c", "python3 start_unified.py --profile ${PROFILE:-balanced}"]
CMD ["sh", "-c", "python3 start_unified.pyc --profile ${PROFILE:-balanced}"]

View File

@ -4,6 +4,7 @@ import time
import copy
import os
import tempfile
import asyncio
from pathlib import Path
from typing import Any, Dict
from langchain.chat_models import init_chat_model
@ -29,7 +30,10 @@ from utils.settings import (
TOOL_OUTPUT_MAX_LENGTH,
MCP_HTTP_TIMEOUT,
MCP_SSE_READ_TIMEOUT,
DEFAULT_TRIM_TOKEN_LIMIT
DEFAULT_TRIM_TOKEN_LIMIT,
DAYTONA_API_KEY,
DAYTONA_SERVER_URL,
DAYTONA_ENABLED,
)
from utils.token_counter import create_token_counter
from agent.agent_config import AgentConfig
@ -170,6 +174,10 @@ async def get_tools_from_mcp(mcp):
logger.error(f"get_tools_from_mcp: error {e}, elapsed: {time.time() - start_time:.3f}s")
return []
from utils.daytona_sync import init_daytona_sandbox, sync_sandbox_to_local
async def init_agent(config: AgentConfig):
"""
初始化 Agent支持持久化内存和对话摘要
@ -184,19 +192,27 @@ async def init_agent(config: AgentConfig):
(agent, checkpointer) 元组
"""
# 加载配置
final_system_prompt = await load_system_prompt_async(config)
final_mcp_settings = await load_mcp_settings_async(config)
create_start = time.time()
# 并行加载配置
final_system_prompt, final_mcp_settings = await asyncio.gather(
load_system_prompt_async(config),
load_mcp_settings_async(config),
)
logger.info(f"init_agent config loaded, elapsed: {time.time() - create_start:.3f}s")
mcp_settings = final_mcp_settings if final_mcp_settings else []
system_prompt = final_system_prompt if final_system_prompt else read_system_prompt()
config.system_prompt = mcp_settings
config.mcp_settings = system_prompt
config.system_prompt = system_prompt
config.mcp_settings = mcp_settings
# 获取 mcp_tools缓存逻辑已内置到 get_tools_from_mcp 中)
mcp_tools = await get_tools_from_mcp(mcp_settings)
logger.info(f"Loaded {len(mcp_tools)} MCP tools")
workspace_root = str(Path.cwd() / "projects" /"robot"/ config.bot_id)
local_workspace_root = workspace_root
# 并行执行高耗时 IOMCP tools 加载 + Daytona sandbox 初始化
mcp_tools_task = asyncio.create_task(get_tools_from_mcp(mcp_settings))
sandbox_task = asyncio.create_task(asyncio.to_thread(init_daytona_sandbox, config.bot_id, local_workspace_root))
# 检测或使用指定的提供商
model_provider, base_url = detect_provider(config.model_name, config.model_server)
@ -222,7 +238,6 @@ async def init_agent(config: AgentConfig):
logger.info(f"Creating new agent for session: {getattr(config, 'session_id', 'no-session')}")
checkpointer = None
create_start = time.time()
# 从连接池获取 checkpointerprepare_checkpoint_message 已在 from_v1/from_v2_request 中调用)
if config.session_id:
@ -234,41 +249,33 @@ async def init_agent(config: AgentConfig):
# 构建中间件列表
middleware = []
# 添加空响应重试中间件(最先执行,最外层包裹)
middleware.append(EmptyResponseRetryMiddleware())
# 首先添加 ToolUseCleanupMiddleware 来清理孤立的 tool_use
middleware.append(ToolUseCleanupMiddleware())
# 添加工具输出长度控制中间件
tool_output_middleware = ToolOutputLengthMiddleware(
max_length=(getattr(config.generate_cfg, 'tool_output_max_length', None) if config.generate_cfg else None) or TOOL_OUTPUT_MAX_LENGTH,
truncation_strategy=getattr(config.generate_cfg, 'tool_output_truncation_strategy', 'smart') if config.generate_cfg else 'smart',
tool_filters=getattr(config.generate_cfg, 'tool_output_filters', None) if config.generate_cfg else None,
exclude_tools=getattr(config.generate_cfg, 'tool_output_exclude', []) if config.generate_cfg else [],
preserve_code_blocks=getattr(config.generate_cfg, 'preserve_code_blocks', True) if config.generate_cfg else True,
preserve_json=getattr(config.generate_cfg, 'preserve_json', True) if config.generate_cfg else True
)
middleware.append(tool_output_middleware)
# tool_output_middleware = ToolOutputLengthMiddleware(
# max_length=(getattr(config.generate_cfg, 'tool_output_max_length', None) if config.generate_cfg else None) or TOOL_OUTPUT_MAX_LENGTH,
# truncation_strategy=getattr(config.generate_cfg, 'tool_output_truncation_strategy', 'smart') if config.generate_cfg else 'smart',
# tool_filters=getattr(config.generate_cfg, 'tool_output_filters', None) if config.generate_cfg else None,
# exclude_tools=getattr(config.generate_cfg, 'tool_output_exclude', []) if config.generate_cfg else [],
# preserve_code_blocks=getattr(config.generate_cfg, 'preserve_code_blocks', True) if config.generate_cfg else True,
# preserve_json=getattr(config.generate_cfg, 'preserve_json', True) if config.generate_cfg else True
# )
# middleware.append(tool_output_middleware)
# 添加 Mem0 记忆中间件(如果启用)
if config.enable_memori:
try:
# 确保有 user_identifier
if not config.user_identifier:
logger.warning("Mem0 enabled but user_identifier is missing, skipping Mem0")
else:
# 获取全局 Mem0Manager已在 fastapi_app.py 中初始化)
mem0_manager = get_mem0_manager()
# 创建 Mem0 中间件,传入现有的 llm_instance 和 config
mem0_middleware = create_mem0_middleware(
bot_id=config.bot_id,
user_identifier=config.user_identifier,
session_id=config.session_id or "default",
agent_config=config, # 传入 AgentConfig 用于中间件间传递数据
agent_config=config,
enabled=config.enable_memori,
semantic_search_top_k=config.memori_semantic_search_top_k,
mem0_manager=mem0_manager,
llm_instance=llm_instance, # 传入现有 LLM 实例
llm_instance=llm_instance,
)
if mem0_middleware:
@ -278,7 +285,6 @@ async def init_agent(config: AgentConfig):
except Exception as e:
logger.error(f"Failed to create Mem0 middleware: {e}, continuing without Mem0")
# 只有在 enable_thinking 为 True 时才添加 GuidelineMiddleware
if config.enable_thinking:
middleware.append(GuidelineMiddleware(llm_instance, config, system_prompt))
@ -290,8 +296,14 @@ async def init_agent(config: AgentConfig):
token_counter=create_token_counter(config.model_name)
)
middleware.append(summarization_middleware)
workspace_root = str(Path.cwd() / "projects" /"robot"/ config.bot_id)
# workspace_root = str(Path.home() / ".deepagents" / config.bot_id)
logger.info(f"init_agent middleware ready, elapsed: {time.time() - create_start:.3f}s")
mcp_tools = await mcp_tools_task
logger.info(f"Loaded {len(mcp_tools)} MCP tools")
logger.info(f"init_agent mcp tools ready, elapsed: {time.time() - create_start:.3f}s")
sandbox, sandbox_type, workspace_root = await sandbox_task
logger.info(f"init_agent sandbox ready, elapsed: {time.time() - create_start:.3f}s")
agent, composite_backend = create_custom_cli_agent(
model=llm_instance,
@ -302,6 +314,8 @@ async def init_agent(config: AgentConfig):
workspace_root=workspace_root,
middleware=middleware,
checkpointer=checkpointer,
sandbox=sandbox,
sandbox_type=sandbox_type,
shell_env={
"ASSISTANT_ID": config.bot_id,
"USER_IDENTIFIER": config.user_identifier,
@ -312,7 +326,7 @@ async def init_agent(config: AgentConfig):
)
logger.info(f"create agent elapsed: {time.time() - create_start:.3f}s")
return agent, checkpointer
return agent, checkpointer, sandbox
class CustomSkillsMiddleware(SkillsMiddleware):
@ -474,7 +488,7 @@ def create_custom_cli_agent(
# Add skills middleware
if enable_skills:
skills_sources = ["/skills"]
skills_sources = ["/workspace/skills"]
agent_middleware.append(
CustomSkillsMiddleware(

View File

@ -8,7 +8,7 @@ import asyncio
from typing import List, Dict, Optional, Any
from datetime import datetime, timezone, timedelta
import logging
from utils.settings import BACKEND_HOST, MASTERKEY
from utils.settings import BACKEND_HOST, MASTERKEY, DAYTONA_ENABLED
logger = logging.getLogger('app')
from .plugin_hook_loader import execute_hooks, merge_skill_mcp_configs
from pathlib import Path
@ -118,22 +118,20 @@ async def load_system_prompt_async(config) -> str:
readme_path = os.path.join(project_dir, "README.md")
readme = await config_cache.get_text_file(readme_path) or ""
# ============ 执行 PrePrompt hooks ============
hook_content = await execute_hooks('PrePrompt', config)
# agent_dir_path = f"~/.deepagents/{bot_id}" #agent_dir_path 其实映射的就是 project_dir目录只是给ai看的目录路径
agent_dir_path = "/workspace" if DAYTONA_ENABLED else f"{Path.cwd()}/projects/robot/{config.bot_id}"
prompt = system_prompt_default.format(
readme=str(readme),
extra_prompt=system_prompt or "",
language=language_display,
user_identifier=user_identifier,
datetime=datetime_str,
agent_dir_path=f"{Path.cwd()}/projects/robot/{config.bot_id}",
trace_id=trace_id or ""
agent_dir_path=agent_dir_path,
trace_id=trace_id or "",
hook_content=f"# Context from Skills\n\n{hook_content}" if hook_content else ""
)
# ============ 执行 PrePrompt hooks ============
hook_content = await execute_hooks('PrePrompt', config)
if hook_content:
# 将hook内容注入到prompt的末尾
prompt = f"{prompt}\n\n## Context from Skills\n\n{hook_content}"
return prompt or ""

461
poetry.lock generated
View File

@ -184,6 +184,21 @@ yarl = ">=1.17.0,<2.0"
[package.extras]
speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
[[package]]
name = "aiohttp-retry"
version = "2.9.1"
description = "Simple retry client for aiohttp"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "aiohttp_retry-2.9.1-py3-none-any.whl", hash = "sha256:66d2759d1921838256a05a3f80ad7e724936f083e35be5abb5e16eed6be6dc54"},
{file = "aiohttp_retry-2.9.1.tar.gz", hash = "sha256:8eb75e904ed4ee5c2ec242fefe85bf04240f685391c4879d8f541d6028ff01f1"},
]
[package.dependencies]
aiohttp = "*"
[[package]]
name = "aiosignal"
version = "1.4.0"
@ -736,6 +751,146 @@ ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi (>=2024)", "cryptography-vectors (==46.0.5)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
test-randomorder = ["pytest-randomly"]
[[package]]
name = "daytona"
version = "0.168.0"
description = "Python SDK for Daytona"
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "daytona-0.168.0-py3-none-any.whl", hash = "sha256:c771d167f039646fa722dcf14ec0db0df4a5d08db87980147eaa492af19f5a14"},
{file = "daytona-0.168.0.tar.gz", hash = "sha256:ed9a6679a719f8f9f1f0258232a958a52f1f71c4f598e53e7c871e696ad81a40"},
]
[package.dependencies]
aiofiles = ">=24.1.0,<24.2.0"
daytona-api-client = "0.168.0"
daytona-api-client-async = "0.168.0"
daytona-toolbox-api-client = "0.168.0"
daytona-toolbox-api-client-async = "0.168.0"
Deprecated = ">=1.2.18,<2.0.0"
httpx = ">=0.28.0,<0.29.0"
obstore = ">=0.8.0,<0.9.0"
opentelemetry-api = ">=1.27.0,<2.0.0"
opentelemetry-exporter-otlp-proto-http = ">=1.27.0,<2.0.0"
opentelemetry-instrumentation-aiohttp-client = ">=0.59b0"
opentelemetry-sdk = ">=1.27.0,<2.0.0"
pydantic = ">=2.4.2,<3.0.0"
python-dotenv = ">=1.0.0,<2.0.0"
python-multipart = ">=0.0.15,<0.1.0"
toml = ">=0.10.0,<0.11.0"
urllib3 = ">=2.1.0,<3.0.0"
websockets = ">=15.0.0,<16.0.0"
[[package]]
name = "daytona-api-client"
version = "0.168.0"
description = "Daytona"
optional = false
python-versions = "<4.0,>=3.8"
groups = ["main"]
files = [
{file = "daytona_api_client-0.168.0-py3-none-any.whl", hash = "sha256:bab0391c03a52ad238237b4f32ad38798bcd89ce4fde75e0905a1d28d66020de"},
{file = "daytona_api_client-0.168.0.tar.gz", hash = "sha256:66027ecbeb16818a7622a20fe2d140360814315f4fd8afcfbf48b63d285af528"},
]
[package.dependencies]
pydantic = ">=2"
python-dateutil = ">=2.8.2"
typing-extensions = ">=4.7.1"
urllib3 = ">=2.1.0,<3.0.0"
[[package]]
name = "daytona-api-client-async"
version = "0.168.0"
description = "Daytona"
optional = false
python-versions = "<4.0,>=3.8"
groups = ["main"]
files = [
{file = "daytona_api_client_async-0.168.0-py3-none-any.whl", hash = "sha256:ab4cc748bf04627744c68ca861329b965649e4dffff9e39e4ee1d3ff5a10a79c"},
{file = "daytona_api_client_async-0.168.0.tar.gz", hash = "sha256:b0aac0269f1f61f8d7859706b883883d703c7802ad559027f96e4ab309523a82"},
]
[package.dependencies]
aiohttp = ">=3.8.4"
aiohttp-retry = ">=2.8.3"
pydantic = ">=2"
python-dateutil = ">=2.8.2"
typing-extensions = ">=4.7.1"
urllib3 = ">=2.1.0,<3.0.0"
[[package]]
name = "daytona-sdk"
version = "0.168.0"
description = "Python SDK for Daytona"
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "daytona_sdk-0.168.0-py3-none-any.whl", hash = "sha256:6fa50880689534dd3be9c2a15e54b31d5af9e4013b0d9cfab649741e748beae4"},
{file = "daytona_sdk-0.168.0.tar.gz", hash = "sha256:a7d3a31e57b9374f11512f5d1245d11b94aa2b1b2795a1a1aa69056ff22096e4"},
]
[package.dependencies]
aiofiles = ">=24.1.0,<24.2.0"
daytona-api-client = "0.168.0"
daytona-api-client-async = "0.168.0"
daytona-toolbox-api-client = "0.168.0"
daytona-toolbox-api-client-async = "0.168.0"
Deprecated = ">=1.2.18,<2.0.0"
httpx = ">=0.28.0,<0.29.0"
obstore = ">=0.8.0,<0.9.0"
opentelemetry-api = ">=1.27.0,<2.0.0"
opentelemetry-exporter-otlp-proto-http = ">=1.27.0,<2.0.0"
opentelemetry-instrumentation-aiohttp-client = ">=0.59b0"
opentelemetry-sdk = ">=1.27.0,<2.0.0"
pydantic = ">=2.4.2,<3.0.0"
python-dotenv = ">=1.0.0,<2.0.0"
python-multipart = ">=0.0.15,<0.1.0"
toml = ">=0.10.0,<0.11.0"
urllib3 = ">=2.1.0,<3.0.0"
websockets = ">=15.0.0,<16.0.0"
[[package]]
name = "daytona-toolbox-api-client"
version = "0.168.0"
description = "Daytona Toolbox API"
optional = false
python-versions = "<4.0,>=3.8"
groups = ["main"]
files = [
{file = "daytona_toolbox_api_client-0.168.0-py3-none-any.whl", hash = "sha256:a4c0b58a6e1f36eaa8249246380afcbc5851b1c31c3f1b3d3e4fa714428747f7"},
{file = "daytona_toolbox_api_client-0.168.0.tar.gz", hash = "sha256:d675a021750b7d9d5ac2cfdb6ed31bebd13c5d4e9704405d7561498520235678"},
]
[package.dependencies]
pydantic = ">=2"
python-dateutil = ">=2.8.2"
typing-extensions = ">=4.7.1"
urllib3 = ">=2.1.0,<3.0.0"
[[package]]
name = "daytona-toolbox-api-client-async"
version = "0.168.0"
description = "Daytona Toolbox API"
optional = false
python-versions = "<4.0,>=3.8"
groups = ["main"]
files = [
{file = "daytona_toolbox_api_client_async-0.168.0-py3-none-any.whl", hash = "sha256:06977245e7b723b296634991b2ecf203196ecae5fa50485e5a3e9a4757a253e4"},
{file = "daytona_toolbox_api_client_async-0.168.0.tar.gz", hash = "sha256:bc3dc6d2b3dd5b4a38f1d66a8fe309433fcf633642869435ac89db9ea6612ab1"},
]
[package.dependencies]
aiohttp = ">=3.8.4"
aiohttp-retry = ">=2.8.3"
pydantic = ">=2"
python-dateutil = ">=2.8.2"
typing-extensions = ">=4.7.1"
urllib3 = ">=2.1.0,<3.0.0"
[[package]]
name = "deepagents"
version = "0.5.2"
@ -855,6 +1010,24 @@ files = [
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
]
[[package]]
name = "deprecated"
version = "1.3.1"
description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
groups = ["main"]
files = [
{file = "deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f"},
{file = "deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223"},
]
[package.dependencies]
wrapt = ">=1.10,<3"
[package.extras]
dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"]
[[package]]
name = "distro"
version = "1.9.0"
@ -2081,6 +2254,22 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10.0.0"
typing-extensions = ">=4.7.0,<5.0.0"
uuid-utils = ">=0.12.0,<1.0"
[[package]]
name = "langchain-daytona"
version = "0.0.5"
description = "Daytona sandbox integration for Deep Agents"
optional = false
python-versions = "<4.0,>=3.11"
groups = ["main"]
files = [
{file = "langchain_daytona-0.0.5-py3-none-any.whl", hash = "sha256:eaf271f2b09d3881b0d58bbc74eef1ac8d2d1987f193abf052b58e1c6b4b5900"},
{file = "langchain_daytona-0.0.5.tar.gz", hash = "sha256:6b5d2ab0afc0678aa179e536a54732bac3ce6280f9dd9d1faa24cb3ee4b2e843"},
]
[package.dependencies]
daytona = "*"
deepagents = ">=0.5.0,<0.6.0"
[[package]]
name = "langchain-google-genai"
version = "4.2.1"
@ -3020,6 +3209,109 @@ files = [
{file = "nvidia_nvtx_cu12-12.1.105-py3-none-win_amd64.whl", hash = "sha256:65f4d98982b31b60026e0e6de73fbdfc09d08a96f4656dd3665ca616a11e1e82"},
]
[[package]]
name = "obstore"
version = "0.8.2"
description = ""
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "obstore-0.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:49104c0d72688c180af015b02c691fbb6cf6a45b03a9d71b84059ed92dbec704"},
{file = "obstore-0.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c49776abd416e4d80d003213522d82ad48ed3517bee27a6cf8ce0f0cf4e6337e"},
{file = "obstore-0.8.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1636372b5e171a98369612d122ea20b955661daafa6519ed8322f4f0cb43ff74"},
{file = "obstore-0.8.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2efed0d86ad4ebffcbe3d0c4d84f26c2c6b20287484a0a748499c169a8e1f2c4"},
{file = "obstore-0.8.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00c5542616dc5608de82ab6f6820633c9dbab6ff048e770fb8a5fcd1d30cd656"},
{file = "obstore-0.8.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d9df46aaf25ce80fff48c53382572adc67b6410611660b798024450281a3129"},
{file = "obstore-0.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ccf0f03a7fe453fb8640611c922bce19f021c6aaeee6ee44d6d8fb57db6be48"},
{file = "obstore-0.8.2-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:ddfbfadc88c5e9740b687ef0833384329a56cea07b34f44e1c4b00a0e97d94a9"},
{file = "obstore-0.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:53ad53bb16e64102f39559ec470efd78a5272b5e3b84c53aa0423993ac5575c1"},
{file = "obstore-0.8.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:b0b905b46354db0961ab818cad762b9c1ac154333ae5d341934c90635a6bd7ab"},
{file = "obstore-0.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fee235694406ebb2dc4178752cf5587f471d6662659b082e9786c716a0a9465c"},
{file = "obstore-0.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6c36faf7ace17dd0832aa454118a63ea21862e3d34f71b9297d0c788d00f4985"},
{file = "obstore-0.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:948a1db1d34f88cfc7ab7e0cccdcfd84cf3977365634599c95ba03b4ef80d1c4"},
{file = "obstore-0.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2edaa97687c191c5324bb939d72f6fe86a7aa8191c410f1648c14e8296d05c1c"},
{file = "obstore-0.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c4fb7ef8108f08d14edc8bec9e9a6a2e5c4d14eddb8819f5d0da498aff6e8888"},
{file = "obstore-0.8.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fda8f658c0edf799ab1e264f9b12c7c184cd09a5272dc645d42e987810ff2772"},
{file = "obstore-0.8.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87fe2bc15ce4051ecb56abd484feca323c2416628beb62c1c7b6712114564d6e"},
{file = "obstore-0.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2482aa2562ab6a4ca40250b26bea33f8375b59898a9b5615fd412cab81098123"},
{file = "obstore-0.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4153b928f5d2e9c6cb645e83668a53e0b42253d1e8bcb4e16571fc0a1434599a"},
{file = "obstore-0.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbfa9c38620cc191be98c8b5558c62071e495dc6b1cc724f38293ee439aa9f92"},
{file = "obstore-0.8.2-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:0822836eae8d52499f10daef17f26855b4c123119c6eb984aa4f2d525ec2678d"},
{file = "obstore-0.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ef6435dfd586d83b4f778e7927a5d5b0d8b771e9ba914bc809a13d7805410e6"},
{file = "obstore-0.8.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0f2cba91f4271ca95a932a51aa8dda1537160342b33f7836c75e1eb9d40621a2"},
{file = "obstore-0.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:23c876d603af0627627808d19a58d43eb5d8bfd02eecd29460bc9a58030fed55"},
{file = "obstore-0.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ff3c4b5d07629b70b9dee494cd6b94fff8465c3864752181a1cb81a77190fe42"},
{file = "obstore-0.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:aadb2cb72de7227d07f4570f82729625ffc77522fadca5cf13c3a37fbe8c8de9"},
{file = "obstore-0.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bb70ce297a47392b1d9a3e310f18d59cd5ebbb9453428210fef02ed60e4d75d1"},
{file = "obstore-0.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1619bf618428abf1f607e0b219b2e230a966dcf697b717deccfa0983dd91f646"},
{file = "obstore-0.8.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a4605c3ed7c9515aeb4c619b5f7f2c9986ed4a79fe6045e536b5e59b804b1476"},
{file = "obstore-0.8.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce42670417876dd8668cbb8659e860e9725e5f26bbc86449fd259970e2dd9d18"},
{file = "obstore-0.8.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4a3e893b2a06585f651c541c1972fe1e3bf999ae2a5fda052ee55eb7e6516f5"},
{file = "obstore-0.8.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08462b32f95a9948ed56ed63e88406e2e5a4cae1fde198f9682e0fb8487100ed"},
{file = "obstore-0.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a0bf7763292a8fc47d01cd66e6f19002c5c6ad4b3ed4e6b2729f5e190fa8a0d"},
{file = "obstore-0.8.2-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:bcd47f8126cb192cbe86942b8f73b1c45a651ce7e14c9a82c5641dfbf8be7603"},
{file = "obstore-0.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:57eda9fd8c757c3b4fe36cf3918d7e589cc1286591295cc10b34122fa36dd3fd"},
{file = "obstore-0.8.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ea44442aad8992166baa69f5069750979e4c5d9ffce772e61565945eea5774b9"},
{file = "obstore-0.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:41496a3ab8527402db4142aaaf0d42df9d7d354b13ba10d9c33e0e48dd49dd96"},
{file = "obstore-0.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43da209803f052df96c7c3cbec512d310982efd2407e4a435632841a51143170"},
{file = "obstore-0.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:1836f5dcd49f9f2950c75889ab5c51fb290d3ea93cdc39a514541e0be3af016e"},
{file = "obstore-0.8.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:212f033e53fe6e53d64957923c5c88949a400e9027f7038c705ec2e9038be563"},
{file = "obstore-0.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bee21fa4ba148d08fa90e47a96df11161661ed31e09c056a373cb2154b0f2852"},
{file = "obstore-0.8.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c66594b59832ff1ced4c72575d9beb8b5f9b4e404ac1150a42bfb226617fd50"},
{file = "obstore-0.8.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:089f33af5c2fe132d00214a0c1f40601b28f23a38e24ef9f79fb0576f2730b74"},
{file = "obstore-0.8.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d87f658dfd340d5d9ea2d86a7c90d44da77a0db9e00c034367dca335735110cf"},
{file = "obstore-0.8.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e2e4fa92828c4fbc2d487f3da2d3588701a1b67d9f6ca3c97cc2afc912e9c63"},
{file = "obstore-0.8.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab440e89c5c37a8ec230857dd65147d4b923e0cada33297135d05e0f937d696a"},
{file = "obstore-0.8.2-cp313-cp313-manylinux_2_24_aarch64.whl", hash = "sha256:b9beed107c5c9cd995d4a73263861fcfbc414d58773ed65c14f80eb18258a932"},
{file = "obstore-0.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b75b4e7746292c785e31edcd5aadc8b758238372a19d4c5e394db5c305d7d175"},
{file = "obstore-0.8.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:f33e6c366869d05ab0b7f12efe63269e631c5450d95d6b4ba4c5faf63f69de70"},
{file = "obstore-0.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:12c885a9ce5ceb09d13cc186586c0c10b62597eff21b985f6ce8ff9dab963ad3"},
{file = "obstore-0.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4accc883b93349a81c9931e15dd318cc703b02bbef2805d964724c73d006d00e"},
{file = "obstore-0.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:ec850adf9980e5788a826ccfd5819989724e2a2f712bfa3258e85966c8d9981e"},
{file = "obstore-0.8.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1431e40e9bb4773a261e51b192ea6489d0799b9d4d7dbdf175cdf813eb8c0503"},
{file = "obstore-0.8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ddb39d4da303f50b959da000aa42734f6da7ac0cc0be2d5a7838b62c97055bb9"},
{file = "obstore-0.8.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e01f4e13783db453e17e005a4a3ceff09c41c262e44649ba169d253098c775e8"},
{file = "obstore-0.8.2-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df0fc2d0bc17caff9b538564ddc26d7616f7e8b7c65b1a3c90b5048a8ad2e797"},
{file = "obstore-0.8.2-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e439d06c99a140348f046c9f598ee349cc2dcd9105c15540a4b231f9cc48bbae"},
{file = "obstore-0.8.2-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e37d9046669fcc59522d0faf1d105fcbfd09c84cccaaa1e809227d8e030f32c"},
{file = "obstore-0.8.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2646fdcc4bbe92dc2bb5bcdff15574da1211f5806c002b66d514cee2a23c7cb8"},
{file = "obstore-0.8.2-cp314-cp314-manylinux_2_24_aarch64.whl", hash = "sha256:e31a7d37675056d93dfc244605089dee67f5bba30f37c88436623c8c5ad9ba9d"},
{file = "obstore-0.8.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:656313dd8170dde0f0cd471433283337a63912e8e790a121f7cc7639c83e3816"},
{file = "obstore-0.8.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329038c9645d6d1741e77fe1a53e28a14b1a5c1461cfe4086082ad39ebabf981"},
{file = "obstore-0.8.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1e4df99b369790c97c752d126b286dc86484ea49bff5782843a265221406566f"},
{file = "obstore-0.8.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9e1c65c65e20cc990414a8a9af88209b1bbc0dd9521b5f6b0293c60e19439bb7"},
{file = "obstore-0.8.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:2ca19d5310ba2736a3052d756e682cc1aafbcc4069e62c05b7222b7d8434b543"},
{file = "obstore-0.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e5f3df9b64c683e288fa1e47fac237c6a1e1021e7c8cadcc75f1bcb3098e824d"},
{file = "obstore-0.8.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0cd293ace46ee175b50e21c0d8c94f606de6cd68f2f199877c55fe8837c585a5"},
{file = "obstore-0.8.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5a39750feedf5b95b4f62bacaded0b95a53be047d9462d6b24dc8f8b6fc6ec8"},
{file = "obstore-0.8.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb76517cca57f6ee9d74be18074a1c0f5ff0e62b4c6e1e0f893993dda93ebbfc"},
{file = "obstore-0.8.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7cd653932bbb7afe611786388cdb403a4b19b13205e0e43d8b0e4890e0accfd0"},
{file = "obstore-0.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4952d69843bb78c73c9a81258f448003f74ff7b298a60899f015788db98a1cd1"},
{file = "obstore-0.8.2-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:2e3cd6d0822888b7e79c92c1258997289ebf0224598aad8f46ada17405666852"},
{file = "obstore-0.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:feb4a6e5a3f2d323b3f61356d4ef99dd3f430aaacdaf5607ced5f857d992d2d4"},
{file = "obstore-0.8.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:61e29fd6a27df284027c23dc49851dbeeacb2d40cb3d945bd3d6ec6cb0650450"},
{file = "obstore-0.8.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8f9e18ff6c32997bd9a9fd636a98439bcbd3f44f13bae350243eacfb75803161"},
{file = "obstore-0.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6ebc814302485d453b61df956c09662ebb33471684add5bbc321de7ba265b723"},
{file = "obstore-0.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:36478c16fd7c7f880f28ece352251eec1fc6f6b69dbf2b78cec9754eb80a4b41"},
{file = "obstore-0.8.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6ea04118980a9c22fc8581225ff4507b6a161baf8949d728d96e68326ebaab59"},
{file = "obstore-0.8.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5f33a7570b6001b54252260fbec18c3f6d21e25d3ec57e9b6c5e7330e8290eb2"},
{file = "obstore-0.8.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11fa78dfb749edcf5a041cd6db20eae95b3e8b09dfdd9b38d14939da40e7c115"},
{file = "obstore-0.8.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:872bc0921ff88305884546ba05e258ccd95672a03d77db123f0d0563fd3c000b"},
{file = "obstore-0.8.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72556a2fbf018edd921286283e5c7eec9f69a21c6d12516d8a44108eceaa526a"},
{file = "obstore-0.8.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75fa1abf21499dfcfb0328941a175f89a9aa58245bf00e3318fe928e4b10d297"},
{file = "obstore-0.8.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f54f72f30cd608c4399679781c884bf8a0e816c1977a2fac993bf5e1fb30609f"},
{file = "obstore-0.8.2-pp310-pypy310_pp73-manylinux_2_24_aarch64.whl", hash = "sha256:b044ebf1bf7b8f7b0ca309375c1cd9e140be79e072ae8c70bbd5d9b2ad1f7678"},
{file = "obstore-0.8.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:b1326cd2288b64d6fe8857cc22d3a8003b802585fc0741eff2640a8dc35e8449"},
{file = "obstore-0.8.2-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:ba6863230648a9b0e11502d2745d881cf74262720238bc0093c3eabd22a3b24c"},
{file = "obstore-0.8.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:887615da9eeefeb2df849d87c380e04877487aa29dbeb367efc3f17f667470d3"},
{file = "obstore-0.8.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4eec1fb32ffa4fb9fe9ad584611ff031927a5c22732b56075ee7204f0e35ebdf"},
{file = "obstore-0.8.2.tar.gz", hash = "sha256:a467bc4e97169e2ba749981b4fd0936015428d9b8f3fb83a5528536b1b6f377f"},
]
[package.dependencies]
typing-extensions = {version = "*", markers = "python_full_version < \"3.13.0\""}
[[package]]
name = "openai"
version = "2.31.0"
@ -3118,6 +3410,46 @@ typing-extensions = ">=4.5.0"
[package.extras]
gcp-auth = ["opentelemetry-exporter-credential-provider-gcp (>=0.59b0)"]
[[package]]
name = "opentelemetry-instrumentation"
version = "0.62b0"
description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "opentelemetry_instrumentation-0.62b0-py3-none-any.whl", hash = "sha256:30d4e76486eae64fb095264a70c2c809c4bed17b73373e53091470661f7d477c"},
{file = "opentelemetry_instrumentation-0.62b0.tar.gz", hash = "sha256:aa1b0b9ab2e1722c2a8a5384fb016fc28d30bba51826676c8036074790d2861e"},
]
[package.dependencies]
opentelemetry-api = ">=1.4,<2.0"
opentelemetry-semantic-conventions = "0.62b0"
packaging = ">=18.0"
wrapt = ">=1.0.0,<3.0.0"
[[package]]
name = "opentelemetry-instrumentation-aiohttp-client"
version = "0.62b0"
description = "OpenTelemetry aiohttp client instrumentation"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "opentelemetry_instrumentation_aiohttp_client-0.62b0-py3-none-any.whl", hash = "sha256:7f30e9252870c3264b1c00f7c567c8d34b5ff28808423eca8fdbb87c4e528b6b"},
{file = "opentelemetry_instrumentation_aiohttp_client-0.62b0.tar.gz", hash = "sha256:c614ca05a2ef513356284db50f442d52fd8b79dbe83b43f630848353f3aff21a"},
]
[package.dependencies]
opentelemetry-api = ">=1.12,<2.0"
opentelemetry-instrumentation = "0.62b0"
opentelemetry-semantic-conventions = "0.62b0"
opentelemetry-util-http = "0.62b0"
wrapt = ">=1.0.0,<3.0.0"
[package.extras]
instruments = ["aiohttp (>=3.0,<4.0)"]
[[package]]
name = "opentelemetry-proto"
version = "1.41.0"
@ -3169,6 +3501,18 @@ files = [
opentelemetry-api = "1.41.0"
typing-extensions = ">=4.5.0"
[[package]]
name = "opentelemetry-util-http"
version = "0.62b0"
description = "Web util for OpenTelemetry"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "opentelemetry_util_http-0.62b0-py3-none-any.whl", hash = "sha256:c20462808d8cc95b69b0dc4a3e02a9d36beb663347e96c931f51ffd78bd318ad"},
{file = "opentelemetry_util_http-0.62b0.tar.gz", hash = "sha256:a62e4b19b8a432c0de657f167dee3455516136bb9c6ed463ca8063019970d835"},
]
[[package]]
name = "orjson"
version = "3.11.5"
@ -5574,6 +5918,18 @@ dev = ["tokenizers[testing]"]
docs = ["setuptools-rust", "sphinx", "sphinx-rtd-theme"]
testing = ["black (==22.3)", "datasets", "numpy", "pytest", "pytest-asyncio", "requests", "ruff"]
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
groups = ["main"]
files = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
[[package]]
name = "tomli-w"
version = "1.2.0"
@ -6194,6 +6550,109 @@ files = [
{file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"},
]
[[package]]
name = "wrapt"
version = "2.1.2"
description = "Module for decorators, wrappers and monkey patching."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "wrapt-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a86d99a14f76facb269dc148590c01aaf47584071809a70da30555228158c"},
{file = "wrapt-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a819e39017f95bf7aede768f75915635aa8f671f2993c036991b8d3bfe8dbb6f"},
{file = "wrapt-2.1.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5681123e60aed0e64c7d44f72bbf8b4ce45f79d81467e2c4c728629f5baf06eb"},
{file = "wrapt-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8b28e97a44d21836259739ae76284e180b18abbb4dcfdff07a415cf1016c3e"},
{file = "wrapt-2.1.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cef91c95a50596fcdc31397eb6955476f82ae8a3f5a8eabdc13611b60ee380ba"},
{file = "wrapt-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dad63212b168de8569b1c512f4eac4b57f2c6934b30df32d6ee9534a79f1493f"},
{file = "wrapt-2.1.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d307aa6888d5efab2c1cde09843d48c843990be13069003184b67d426d145394"},
{file = "wrapt-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c87cf3f0c85e27b3ac7d9ad95da166bf8739ca215a8b171e8404a2d739897a45"},
{file = "wrapt-2.1.2-cp310-cp310-win32.whl", hash = "sha256:d1c5fea4f9fe3762e2b905fdd67df51e4be7a73b7674957af2d2ade71a5c075d"},
{file = "wrapt-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:d8f7740e1af13dff2684e4d56fe604a7e04d6c94e737a60568d8d4238b9a0c71"},
{file = "wrapt-2.1.2-cp310-cp310-win_arm64.whl", hash = "sha256:1c6cc827c00dc839350155f316f1f8b4b0c370f52b6a19e782e2bda89600c7dc"},
{file = "wrapt-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96159a0ee2b0277d44201c3b5be479a9979cf154e8c82fa5df49586a8e7679bb"},
{file = "wrapt-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98ba61833a77b747901e9012072f038795de7fc77849f1faa965464f3f87ff2d"},
{file = "wrapt-2.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:767c0dbbe76cae2a60dd2b235ac0c87c9cccf4898aef8062e57bead46b5f6894"},
{file = "wrapt-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c691a6bc752c0cc4711cc0c00896fcd0f116abc253609ef64ef930032821842"},
{file = "wrapt-2.1.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f3b7d73012ea75aee5844de58c88f44cf62d0d62711e39da5a82824a7c4626a8"},
{file = "wrapt-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577dff354e7acd9d411eaf4bfe76b724c89c89c8fc9b7e127ee28c5f7bcb25b6"},
{file = "wrapt-2.1.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d7b6fd105f8b24e5bd23ccf41cb1d1099796524bcc6f7fbb8fe576c44befbc9"},
{file = "wrapt-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:866abdbf4612e0b34764922ef8b1c5668867610a718d3053d59e24a5e5fcfc15"},
{file = "wrapt-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5a0a0a3a882393095573344075189eb2d566e0fd205a2b6414e9997b1b800a8b"},
{file = "wrapt-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:64a07a71d2730ba56f11d1a4b91f7817dc79bc134c11516b75d1921a7c6fcda1"},
{file = "wrapt-2.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:b89f095fe98bc12107f82a9f7d570dc83a0870291aeb6b1d7a7d35575f55d98a"},
{file = "wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9"},
{file = "wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748"},
{file = "wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e"},
{file = "wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8"},
{file = "wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c"},
{file = "wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c"},
{file = "wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1"},
{file = "wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2"},
{file = "wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0"},
{file = "wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63"},
{file = "wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf"},
{file = "wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b"},
{file = "wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e"},
{file = "wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb"},
{file = "wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca"},
{file = "wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267"},
{file = "wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f"},
{file = "wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8"},
{file = "wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413"},
{file = "wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6"},
{file = "wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1"},
{file = "wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf"},
{file = "wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b"},
{file = "wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18"},
{file = "wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d"},
{file = "wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015"},
{file = "wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92"},
{file = "wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf"},
{file = "wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67"},
{file = "wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a"},
{file = "wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd"},
{file = "wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f"},
{file = "wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679"},
{file = "wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9"},
{file = "wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9"},
{file = "wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e"},
{file = "wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c"},
{file = "wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a"},
{file = "wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90"},
{file = "wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586"},
{file = "wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19"},
{file = "wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508"},
{file = "wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04"},
{file = "wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575"},
{file = "wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb"},
{file = "wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22"},
{file = "wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596"},
{file = "wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044"},
{file = "wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b"},
{file = "wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf"},
{file = "wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2"},
{file = "wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3"},
{file = "wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7"},
{file = "wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5"},
{file = "wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00"},
{file = "wrapt-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5e0fa9cc32300daf9eb09a1f5bdc6deb9a79defd70d5356ba453bcd50aef3742"},
{file = "wrapt-2.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:710f6e5dfaf6a5d5c397d2d6758a78fecd9649deb21f1b645f5b57a328d63050"},
{file = "wrapt-2.1.2-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:305d8a1755116bfdad5dda9e771dcb2138990a1d66e9edd81658816edf51aed1"},
{file = "wrapt-2.1.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0d8fc30a43b5fe191cf2b1a0c82bab2571dadd38e7c0062ee87d6df858dd06e"},
{file = "wrapt-2.1.2-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a5d516e22aedb7c9c1d47cba1c63160b1a6f61ec2f3948d127cd38d5cfbb556f"},
{file = "wrapt-2.1.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:45914e8efbe4b9d5102fcf0e8e2e3258b83a5d5fba9f8f7b6d15681e9d29ffe0"},
{file = "wrapt-2.1.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:478282ebd3795a089154fb16d3db360e103aa13d3b2ad30f8f6aac0d2207de0e"},
{file = "wrapt-2.1.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3756219045f73fb28c5d7662778e4156fbd06cf823c4d2d4b19f97305e52819c"},
{file = "wrapt-2.1.2-cp39-cp39-win32.whl", hash = "sha256:b8aefb4dbb18d904b96827435a763fa42fc1f08ea096a391710407a60983ced8"},
{file = "wrapt-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:e5aeab8fe15c3dff75cfee94260dcd9cded012d4ff06add036c28fae7718593b"},
{file = "wrapt-2.1.2-cp39-cp39-win_arm64.whl", hash = "sha256:f069e113743a21a3defac6677f000068ebb931639f789b5b226598e247a4c89e"},
{file = "wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8"},
{file = "wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e"},
]
[package.extras]
dev = ["pytest", "setuptools"]
[[package]]
name = "wsgidav"
version = "4.3.3"
@ -6662,4 +7121,4 @@ cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and pyt
[metadata]
lock-version = "2.1"
python-versions = ">=3.12,<3.15"
content-hash = "c16e750b15c58ccd16f289b20da85a871d7fb4eccee09ccc6cff96c86da21595"
content-hash = "83ac4f26e8b5e844ad670748f9950b9bdf13533e9adb62fe3e1d2a147e85801a"

View File

@ -110,6 +110,18 @@
- 回复格式: "「{元の位置}」では見つかりませんでしたが、{簡略化した位置}エリアで以下の設備が見つかりました。こちらでよろしいですか?"
- **全部失败时**:告知用户未找到设备,建议提供其他位置信息或直接指定房间名
- 回复格式: "申し訳ございません、該当エリアでは操作可能な設備が見つかりませんでした。お部屋の名前をお教えいただけますか?"
6. **处理设备缺失但 area 存在的情况(エリアは見つかったが設備がない場合)**
如果 find_device_by_area 成功匹配了 area但该 area 没有用户请求的设备类型(如空调 dcu、照明 light应主动建议相邻的可控 area
- **第1步**:告知用户该 area 没有请求的设备类型
- **第2步**:基于同楼层/同区域的命名规则,推断相邻的可控 area 并搜索
- 例: "Define Area フラッパーゲート付近" → 搜索 "Define Area 中央窓側"、"Define Area 中央" 等同楼层 Define Area
- 例: "2階会議室A" → 搜索 "2階会議室B"、"2階会議室C" 等同楼层会议室
- **第3步**:如果有找到有该设备类型的可控 area主动向用户建议并确认
- 回复格式: "{エリア名}には{設備タイプ}がありませんが、近くの{別エリア名}ならコントロールできます。こちらでよろしいですか?"
- 日文例: "フラッパーゲート付近は空調がありませんが、近くの Define Area 中央ならコントロールできます。こちらでよろしいですか?"
- **第4步**:如果同区域没找到,告知用户并询问是否需要其他帮助
- 回复格式: "{エリア名}には{設備タイプ}がございません。他のエリアで操作されますか?"
- 日文例: "申し訳ございません、フラッパーゲート付近には空調設備がございません。近くの別エリアをご希望ですか?"
3. 更新设备(此操作需要确认)
- **条件**:用户意图为控制设备或调节参数(如开关、温度、风速), 需要进行确认。

View File

@ -69,6 +69,8 @@ When creating scripts in `executable_code/`, follow these organization rules:
- Temporary script (when needed): `{agent_dir_path}/executable_code/tmp/test.py`
- Downloaded file: `{agent_dir_path}/download/report.pdf`
{hook_content}
# System Information
<env>
Working directory: {agent_dir_path}

View File

@ -39,6 +39,8 @@ dependencies = [
"wsgidav (>=4.3.3,<5.0.0)",
"croniter (>=2.0.0,<4.0.0)",
"pyyaml (>=6.0,<7.0)",
"daytona-sdk",
"langchain-daytona",
]
[tool.poetry.requires-plugins]

View File

@ -1,6 +1,7 @@
agent-client-protocol==0.9.0 ; python_version >= "3.12" and python_version < "3.15"
aiofiles==24.1.0 ; python_version >= "3.12" and python_version < "3.15"
aiohappyeyeballs==2.6.1 ; python_version >= "3.12" and python_version < "3.15"
aiohttp-retry==2.9.1 ; python_version >= "3.12" and python_version < "3.15"
aiohttp==3.13.1 ; python_version >= "3.12" and python_version < "3.15"
aiosignal==1.4.0 ; python_version >= "3.12" and python_version < "3.15"
aiosqlite==0.22.1 ; python_version >= "3.12" and python_version < "3.15"
@ -22,10 +23,17 @@ cloudpickle==3.1.2 ; python_version >= "3.12" and python_version < "3.15"
colorama==0.4.6 ; python_version >= "3.12" and python_version < "3.15" and platform_system == "Windows"
croniter==3.0.4 ; python_version >= "3.12" and python_version < "3.15"
cryptography==46.0.5 ; python_version >= "3.12" and python_version < "3.15"
daytona-api-client-async==0.168.0 ; python_version >= "3.12" and python_version < "3.15"
daytona-api-client==0.168.0 ; python_version >= "3.12" and python_version < "3.15"
daytona-sdk==0.168.0 ; python_version >= "3.12" and python_version < "3.15"
daytona-toolbox-api-client-async==0.168.0 ; python_version >= "3.12" and python_version < "3.15"
daytona-toolbox-api-client==0.168.0 ; python_version >= "3.12" and python_version < "3.15"
daytona==0.168.0 ; python_version >= "3.12" and python_version < "3.15"
deepagents-acp==0.0.5 ; python_version >= "3.12" and python_version < "3.15"
deepagents-cli==0.0.37 ; python_version >= "3.12" and python_version < "3.15"
deepagents==0.5.2 ; python_version >= "3.12" and python_version < "3.15"
defusedxml==0.7.1 ; python_version >= "3.12" and python_version < "3.15"
deprecated==1.3.1 ; python_version >= "3.12" and python_version < "3.15"
distro==1.9.0 ; python_version >= "3.12" and python_version < "3.15"
docstring-parser==0.17.0 ; python_version >= "3.12" and python_version < "3.15"
et-xmlfile==2.0.0 ; python_version >= "3.12" and python_version < "3.15"
@ -67,6 +75,7 @@ jsonschema-specifications==2025.9.1 ; python_version >= "3.12" and python_versio
jsonschema==4.25.1 ; python_version >= "3.12" and python_version < "3.15"
langchain-anthropic==1.4.0 ; python_version >= "3.12" and python_version < "3.15"
langchain-core==1.2.28 ; python_version >= "3.12" and python_version < "3.15"
langchain-daytona==0.0.5 ; python_version >= "3.12" and python_version < "3.15"
langchain-google-genai==4.2.1 ; python_version >= "3.12" and python_version < "3.15"
langchain-mcp-adapters==0.2.1 ; python_version >= "3.12" and python_version < "3.15"
langchain-openai==1.1.12 ; python_version >= "3.12" and python_version < "3.15"
@ -105,14 +114,18 @@ nvidia-cusparse-cu12==12.1.0.106 ; python_version >= "3.12" and python_version <
nvidia-nccl-cu12==2.19.3 ; python_version >= "3.12" and python_version < "3.15" and platform_system == "Linux" and platform_machine == "x86_64"
nvidia-nvjitlink-cu12==12.9.86 ; python_version >= "3.12" and python_version < "3.15" and platform_system == "Linux" and platform_machine == "x86_64"
nvidia-nvtx-cu12==12.1.105 ; python_version >= "3.12" and python_version < "3.15" and platform_system == "Linux" and platform_machine == "x86_64"
obstore==0.8.2 ; python_version >= "3.12" and python_version < "3.15"
openai==2.31.0 ; python_version >= "3.12" and python_version < "3.15"
openpyxl==3.1.5 ; python_version >= "3.12" and python_version < "3.15"
opentelemetry-api==1.41.0 ; python_version >= "3.12" and python_version < "3.15"
opentelemetry-exporter-otlp-proto-common==1.41.0 ; python_version >= "3.12" and python_version < "3.15"
opentelemetry-exporter-otlp-proto-http==1.41.0 ; python_version >= "3.12" and python_version < "3.15"
opentelemetry-instrumentation-aiohttp-client==0.62b0 ; python_version >= "3.12" and python_version < "3.15"
opentelemetry-instrumentation==0.62b0 ; python_version >= "3.12" and python_version < "3.15"
opentelemetry-proto==1.41.0 ; python_version >= "3.12" and python_version < "3.15"
opentelemetry-sdk==1.41.0 ; python_version >= "3.12" and python_version < "3.15"
opentelemetry-semantic-conventions==0.62b0 ; python_version >= "3.12" and python_version < "3.15"
opentelemetry-util-http==0.62b0 ; python_version >= "3.12" and python_version < "3.15"
orjson==3.11.5 ; python_version >= "3.12" and python_version < "3.15"
ormsgpack==1.12.0 ; python_version >= "3.12" and python_version < "3.15"
packaging==25.0 ; python_version >= "3.12" and python_version < "3.15"
@ -176,6 +189,7 @@ textual==8.0.0 ; python_version >= "3.12" and python_version < "3.15"
threadpoolctl==3.6.0 ; python_version >= "3.12" and python_version < "3.15"
tiktoken==0.12.0 ; python_version >= "3.12" and python_version < "3.15"
tokenizers==0.22.1 ; python_version >= "3.12" and python_version < "3.15"
toml==0.10.2 ; python_version >= "3.12" and python_version < "3.15"
tomli-w==1.2.0 ; python_version >= "3.12" and python_version < "3.15"
torch==2.2.0 ; python_version >= "3.12" and python_version < "3.15"
tqdm==4.67.1 ; python_version >= "3.12" and python_version < "3.15"
@ -194,6 +208,7 @@ watchfiles==1.1.1 ; python_version >= "3.12" and python_version < "3.15"
wcmatch==10.1 ; python_version >= "3.12" and python_version < "3.15"
wcwidth==0.2.14 ; python_version >= "3.12" and python_version < "3.15"
websockets==15.0.1 ; python_version >= "3.12" and python_version < "3.15"
wrapt==2.1.2 ; python_version >= "3.12" and python_version < "3.15"
wsgidav==4.3.3 ; python_version >= "3.12" and python_version < "3.15"
xlrd==2.0.2 ; python_version >= "3.12" and python_version < "3.15"
xxhash==3.6.0 ; python_version >= "3.12" and python_version < "3.15"

View File

@ -23,6 +23,8 @@ from langchain_core.messages import AIMessageChunk, ToolMessage, AIMessage, Huma
from utils.settings import MAX_OUTPUT_TOKENS
from agent.agent_config import AgentConfig
from agent.deep_assistant import init_agent
from utils.daytona_sync import sync_sandbox_to_local
from utils.settings import DAYTONA_ENABLED
router = APIRouter()
@ -87,7 +89,7 @@ async def enhanced_generate_stream_response(
logger.info(f"Starting agent stream response")
chunk_id = 0
message_tag = ""
agent, checkpointer = await init_agent(config)
agent, checkpointer, sandbox = await init_agent(config)
async for msg, metadata in agent.astream({"messages": config.messages}, stream_mode="messages", config=config.invoke_config(), max_tokens=MAX_OUTPUT_TOKENS):
# 检查是否收到取消信号
if cancel_event and cancel_event.is_set():
@ -145,7 +147,7 @@ async def enhanced_generate_stream_response(
# ============ 执行 PostAgent hooks ============
# 注意:这里在单独的异步任务中执行,不阻塞流式输出
full_response = "".join(full_response_content)
asyncio.create_task(_execute_post_agent_hooks(config, full_response))
asyncio.create_task(_execute_post_agent_hooks(config, full_response, sandbox))
# ===========================================
await output_queue.put(("agent_done", None))
@ -261,13 +263,13 @@ async def create_agent_and_generate_response(
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}
)
agent, checkpointer = await init_agent(config)
agent, checkpointer, sandbox = await init_agent(config)
# 使用更新后的 messages
agent_responses = await agent.ainvoke({"messages": config.messages}, config=config.invoke_config(), max_tokens=MAX_OUTPUT_TOKENS)
# ============ 执行 PostAgent hooks ============
# 注意:这里在非流式模式下同步执行hooks
await _execute_post_agent_hooks(config, "")
# 注意:这里在单独的异步任务中执行,不阻塞非流式响应
asyncio.create_task(_execute_post_agent_hooks(config, "", sandbox))
# ===========================================
# 从后往前找第一个 HumanMessage之后的内容都给 append_messages
@ -409,13 +411,14 @@ async def _save_assistant_response(config: AgentConfig, assistant_response: str)
logger.error(f"Failed to save assistant response: {e}")
async def _execute_post_agent_hooks(config: AgentConfig, response: str) -> None:
async def _execute_post_agent_hooks(config: AgentConfig, response: str, sandbox=None) -> None:
"""
执行 PostAgent hooks在agent执行后
Args:
config: AgentConfig 对象
response: Agent 的完整响应内容
sandbox: DaytonaSandbox 实例可选用于反向同步文件
"""
try:
from agent.plugin_hook_loader import execute_hooks
@ -436,6 +439,15 @@ async def _execute_post_agent_hooks(config: AgentConfig, response: str) -> None:
# 清理 executable_code/tmp 文件夹
await _cleanup_tmp_folder(config)
# Daytona: 反向同步 sandbox 文件到本地
if sandbox is not None and DAYTONA_ENABLED:
try:
from pathlib import Path
local_workspace = str(Path.cwd() / "projects" / "robot" / config.bot_id)
sync_sandbox_to_local(sandbox, local_workspace)
except Exception as e:
logger.error(f"Failed to sync sandbox to local: {e}")
async def _cleanup_tmp_folder(config: AgentConfig) -> None:
"""

View File

@ -79,9 +79,22 @@ build_deploy_payload() {
local color="$1"
local short_sha="${CIRCLE_SHA1:0:8}"
local message
local content
message=$(commit_message)
content=$(printf '**触发人**: %s\n**分支**: %s\n**Job**: %s\n**服务**: %s\n**命名空间**: %s\n**Commit**: %s\n**完整 SHA**: %s\n**提交信息**: %s' "$CIRCLE_USERNAME" "$CIRCLE_BRANCH" "$CIRCLE_JOB" "$SERVICE_NAME" "$NAMESPACE" "$short_sha" "$CIRCLE_SHA1" "$message" | escape_json)
local username
local branch
local job
local service_name
local namespace
local commit
local full_sha
message=$(commit_message | escape_json)
username=$(printf '%s' "$CIRCLE_USERNAME" | escape_json)
branch=$(printf '%s' "$CIRCLE_BRANCH" | escape_json)
job=$(printf '%s' "$CIRCLE_JOB" | escape_json)
service_name=$(printf '%s' "$SERVICE_NAME" | escape_json)
namespace=$(printf '%s' "$NAMESPACE" | escape_json)
commit=$(printf '%s' "$short_sha" | escape_json)
full_sha=$(printf '%s' "$CIRCLE_SHA1" | escape_json)
cat <<EOF
{
@ -100,10 +113,64 @@ build_deploy_payload() {
"elements": [
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": "${content}"
}
"fields": [
{
"is_short": true,
"text": {
"tag": "lark_md",
"content": "**触发人**\n${username}"
}
},
{
"is_short": true,
"text": {
"tag": "lark_md",
"content": "**分支**\n${branch}"
}
},
{
"is_short": true,
"text": {
"tag": "lark_md",
"content": "**Job**\n${job}"
}
},
{
"is_short": true,
"text": {
"tag": "lark_md",
"content": "**服务**\n${service_name}"
}
},
{
"is_short": true,
"text": {
"tag": "lark_md",
"content": "**命名空间**\n${namespace}"
}
},
{
"is_short": true,
"text": {
"tag": "lark_md",
"content": "**Commit**\n${commit}"
}
},
{
"is_short": false,
"text": {
"tag": "lark_md",
"content": "**完整 SHA**\n${full_sha}"
}
},
{
"is_short": false,
"text": {
"tag": "lark_md",
"content": "**提交信息**\n${message}"
}
}
]
},
{
"tag": "action",
@ -129,9 +196,20 @@ build_docker_hub_payload() {
local color="$1"
local short_sha="${CIRCLE_SHA1:0:8}"
local message
local content
message=$(commit_message)
content=$(printf '**触发人**: %s\n**分支**: %s\n**Job**: %s\n**镜像**: %s:%s\n**版本标签**: %s\n**Commit**: %s\n**完整 SHA**: %s\n**提交信息**: %s' "$CIRCLE_USERNAME" "$CIRCLE_BRANCH" "$CIRCLE_JOB" "$IMAGE_REPO" "$IMAGE_TAG" "$VERSION_TAG" "$short_sha" "$CIRCLE_SHA1" "$message" | escape_json)
local username
local branch
local job
local image
local commit
local full_sha
message=$(commit_message | escape_json)
username=$(printf '%s' "$CIRCLE_USERNAME" | escape_json)
branch=$(printf '%s' "$CIRCLE_BRANCH" | escape_json)
job=$(printf '%s' "$CIRCLE_JOB" | escape_json)
image=$(printf '%s' "$IMAGE_REPO:$VERSION_TAG" | escape_json)
commit=$(printf '%s' "$short_sha" | escape_json)
full_sha=$(printf '%s' "$CIRCLE_SHA1" | escape_json)
cat <<EOF
{
@ -150,10 +228,57 @@ build_docker_hub_payload() {
"elements": [
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": "${content}"
}
"fields": [
{
"is_short": true,
"text": {
"tag": "lark_md",
"content": "**触发人**\n${username}"
}
},
{
"is_short": true,
"text": {
"tag": "lark_md",
"content": "**分支**\n${branch}"
}
},
{
"is_short": true,
"text": {
"tag": "lark_md",
"content": "**Job**\n${job}"
}
},
{
"is_short": true,
"text": {
"tag": "lark_md",
"content": "**镜像**\n${image}"
}
},
{
"is_short": true,
"text": {
"tag": "lark_md",
"content": "**Commit**\n${commit}"
}
},
{
"is_short": false,
"text": {
"tag": "lark_md",
"content": "**完整 SHA**\n${full_sha}"
}
},
{
"is_short": false,
"text": {
"tag": "lark_md",
"content": "**提交信息**\n${message}"
}
}
]
},
{
"tag": "action",

View File

@ -0,0 +1,12 @@
{
"name": "novare-context",
"description": "NOVAREの現在のユーザー詳細情報を自動的に読み込みます",
"hooks": {
"PrePrompt": [
{
"type": "command",
"command": "python hooks/pre_prompt.py"
}
]
}
}

View File

@ -0,0 +1,153 @@
# User Context Loader
用户上下文加载器示例 Skill演示 Claude Plugins 模式的 hooks 机制。
## 功能说明
本 Skill 演示了三种 Hook 类型:
### PrePrompt Hook
在 system_prompt 加载时执行,动态注入用户上下文信息。
- 文件: `hooks/pre_prompt.py`
- 用途: 查询用户信息、偏好设置、历史记录等,注入到 prompt 中
### PostAgent Hook
在 agent 执行完成后执行,用于后处理。
- 文件: `hooks/post_agent.py`
- 用途: 记录分析数据、触发异步任务、发送通知等
### PreSave Hook
在消息保存前执行,用于内容处理。
- 文件: `hooks/pre_save.py`
- 用途: 内容过滤、敏感信息脱敏、格式转换等
## 目录结构
```
user-context-loader/
├── README.md # Skill 说明文档
├── .claude-plugin/
│ └── plugin.json # Hook 和 MCP 配置文件
└── hooks/
├── pre_prompt.py # PrePrompt hook 脚本
├── post_agent.py # PostAgent hook 脚本
└── pre_save.py # PreSave hook 脚本
```
## plugin.json 格式
```json
{
"name": "user-context-loader",
"description": "用户上下文加载器示例 Skill",
"hooks": {
"PrePrompt": [
{
"type": "command",
"command": "python hooks/pre_prompt.py"
}
],
"PostAgent": [
{
"type": "command",
"command": "python hooks/post_agent.py"
}
],
"PreSave": [
{
"type": "command",
"command": "python hooks/pre_save.py"
}
]
},
"mcpServers": {
"server-name": {
"command": "node",
"args": ["path/to/server.js"],
"env": {
"API_KEY": "${API_KEY}"
}
}
}
}
```
## Hook 脚本格式
Hook 脚本通过子进程执行,通过环境变量接收参数,通过 stdout 返回结果。
### 可用环境变量
| 环境变量 | 说明 | 适用于 |
|---------|------|--------|
| `BOT_ID` | Bot ID | 所有 hook |
| `USER_IDENTIFIER` | 用户标识 | 所有 hook |
| `SESSION_ID` | 会话 ID | 所有 hook |
| `LANGUAGE` | 语言代码 | 所有 hook |
| `HOOK_TYPE` | Hook 类型 | 所有 hook |
| `CONTENT` | 消息内容 | PreSave |
| `ROLE` | 消息角色 | PreSave |
| `RESPONSE` | Agent 响应 | PostAgent |
| `METADATA` | 元数据 JSON | PostAgent |
### PrePrompt 示例
```python
#!/usr/bin/env python3
import os
import sys
def main():
user_identifier = os.environ.get('USER_IDENTIFIER', '')
bot_id = os.environ.get('BOT_ID', '')
# 输出要注入到 prompt 中的内容
print(f"## User Context\n\n用户: {user_identifier}")
return 0
if __name__ == '__main__':
sys.exit(main())
```
### PreSave 示例
```python
#!/usr/bin/env python3
import os
import sys
def main():
content = os.environ.get('CONTENT', '')
# 处理内容并输出
print(content) # 输出处理后的内容
return 0
if __name__ == '__main__':
sys.exit(main())
```
### PostAgent 示例
```python
#!/usr/bin/env python3
import os
import sys
def main():
response = os.environ.get('RESPONSE', '')
session_id = os.environ.get('SESSION_ID', '')
# 记录日志(输出到 stderr
print(f"Session {session_id}: Response length {len(response)}", file=sys.stderr)
return 0
if __name__ == '__main__':
sys.exit(main())
```
## 使用场景
1. **PrePrompt**: 用户登录时自动加载其偏好设置、历史订单等
2. **PostAgent**: 记录对话分析数据,触发后续业务流程
3. **PreSave**: 敏感信息脱敏后再存储,如手机号、邮箱等

View File

@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""
PrePrompt Hook - 用户上下文加载器
system_prompt 加载时执行通过 MCP 服务查询用户相关信息并注入到 prompt
"""
import json
import os
import sys
import anyio
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamablehttp_client
# MCP 服务配置
MCP_BASE_URL = "http://prd-mcp.gbase.ai/mcp/iot/sse"
MCP_TIMEOUT = 30
async def get_employee_location(user_identifier: str, bot_id: str) -> dict | None:
"""
通过 MCP 服务查询员工位置信息
Args:
user_identifier: 用户标识员工姓名或邮箱
bot_id: Bot ID当前未使用保留以备将来扩展
Returns:
员工位置信息字典如果查询失败返回 None
"""
try:
async with streamablehttp_client(
url=MCP_BASE_URL,
timeout=MCP_TIMEOUT,
terminate_on_close=False,
) as (read_stream, write_stream, _):
async with ClientSession(read_stream, write_stream) as session:
# 初始化 MCP 会话
await session.initialize()
# 调用 find_employee_location 工具
result = await session.call_tool(
name="find_employee_location",
arguments={"name": user_identifier}
)
# 解析返回结果
if result.content:
for item in result.content:
if hasattr(item, 'text') and item.text:
try:
return json.loads(item.text)
except json.JSONDecodeError:
# 如果不是 JSON直接返回文本
return {"data": item.text}
return None
except Exception:
# 发生错误时返回 None不影响主流程
return None
def format_location_context(location_data: dict | None, user_identifier: str, bot_id: str) -> str:
"""
格式化位置信息为 Markdown 上下文日语
Args:
location_data: MCP 查询返回的位置数据
user_identifier: 用户标识不使用
bot_id: Bot ID不使用
Returns:
格式化后的 Markdown 字符串出错或无数据时返回空字符串
"""
# 出错或无数据时返回空字符串
if not location_data:
return ""
matched_count = location_data.get('matched_count', 0)
results = location_data.get('results', [])
# 没有匹配数据时返回空字符串
if matched_count == 0:
return ""
lines = []
# 添加说明这是当前用户USER_IDENTIFIER的信息
lines.append(f"**Current User ({user_identifier}) Information**:")
lines.append("")
for idx, employee in enumerate(results, 1):
name = employee.get('name', 'Unknown')
sensor_id = employee.get('sensor_id', '')
confidence = employee.get('confidence', 0)
lines.append(f"- Name: {name}")
lines.append(f"- Sensor ID: {sensor_id}")
location_status = employee.get('location_status', '')
if location_status == 'success':
coordinates = employee.get('coordinates', {})
location = employee.get('location', {})
lines.append(f"- Location Status: Success")
lines.append(f"- Floor: {coordinates.get('floor', 'N/A')}")
lines.append(f"- Coordinates: ({coordinates.get('x', 0):.2f}, {coordinates.get('y', 0):.2f}, {coordinates.get('z', 0):.2f})")
if location:
building = location.get('building', '')
area = location.get('area', '')
room = location.get('room', '')
lines.append(f"- Detailed Location: {building} / {area} / {room}")
measurement_time = employee.get('measurement_time')
if measurement_time:
lines.append(f"- Measurement Time: {measurement_time}")
elif location_status == 'not_in_range':
lines.append(f"- Location Status: Out of Range")
error_message = employee.get('error_message', '')
if error_message:
lines.append(f"- Note: {error_message}")
else: # error
lines.append(f"- Location Status: Failed")
error_message = employee.get('error_message', 'Unknown Error')
lines.append(f"- Error: {error_message}")
lines.append("")
return "\n".join(lines)
async def async_main():
"""异步主函数"""
user_identifier = os.environ.get('USER_IDENTIFIER', '')
bot_id = os.environ.get('BOT_ID', '')
if not user_identifier:
return 0
# 查询员工位置信息
location_data = await get_employee_location(user_identifier, bot_id)
# 格式化并输出上下文(出错或无数据时返回空字符串)
context_info = format_location_context(location_data, user_identifier, bot_id)
if context_info:
print(context_info)
return 0
def main():
"""从环境变量读取参数并通过 MCP 服务查询用户上下文"""
try:
return anyio.run(async_main)
except Exception:
# 出错时返回空字符串(不输出任何内容)
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -0,0 +1,22 @@
{
"name": "rag-retrieve-no-citation",
"description": "Only provides rag_retrieve without citation. table_rag_retrieve and local file retrieval are disabled.",
"hooks": {
"PrePrompt": [
{
"type": "command",
"command": "python hooks/pre_prompt.py"
}
]
},
"mcpServers": {
"rag_retrieve": {
"transport": "stdio",
"command": "python",
"args": [
"./rag_retrieve_server.py",
"{bot_id}"
]
}
}
}

View File

@ -0,0 +1,34 @@
# rag-retrieve
只保留 `rag_retrieve` 的精简版插件示例。
## 功能说明
- 通过 `PrePrompt` Hook 注入检索策略
- 暴露 `rag_retrieve` MCP Server
- 插件仅支持 `rag_retrieve`
- 已禁用 `table_rag_retrieve`
- 已禁用本地文件检索
## 目录结构
```text
rag-retrieve-only/
├── README.md
├── .claude-plugin/
│ └── plugin.json
├── hooks/
│ ├── pre_prompt.py
│ └── retrieval-policy.md
├── rag_retrieve_server.py
└── rag_retrieve_tools.json
```
## 当前检索策略
默认顺序skill-enabled knowledge retrieval tools > `rag_retrieve`
- 优先使用可用的技能内知识检索工具
- 不足时使用 `rag_retrieve`
- 不并行执行多个检索源
- 插件仅支持 `rag_retrieve`

View File

@ -0,0 +1,28 @@
#!/usr/bin/env python3
"""
PreMemoryPrompt Hook - 用户上下文加载器示例
在记忆提取提示词FACT_RETRIEVAL_PROMPT加载时执行
根据环境变量决定是否启用禁止使用模型自身知识的 retrieval policy
"""
import os
import sys
from pathlib import Path
def main():
enable_self_knowledge = (
os.getenv("ENABLE_SELF_KNOWLEDGE", "false").lower() == "true"
)
policy_name = (
"retrieval-policy.md"
if enable_self_knowledge
else "retrieval-policy-forbidden-self-knowledge.md"
)
prompt_file = Path(__file__).parent / policy_name
print(prompt_file.read_text(encoding="utf-8"))
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -0,0 +1,111 @@
# Retrieval Policy (Forbidden Self-Knowledge)
## 0. Task Classification
Classify the request before acting:
- **Knowledge retrieval** (facts, summaries, comparisons, prices, lists, timelines, extraction, etc.): follow this policy strictly.
- **Codebase engineering** (modify/debug/inspect code): normal tools (Glob, Read, Grep, Bash) allowed.
- **Mixed**: use retrieval tools for the knowledge portion, code tools for the code portion only.
- **Uncertain**: default to knowledge retrieval.
## 1. Critical Enforcement
For knowledge retrieval tasks, **this policy overrides generic codebase exploration behavior**.
- **Prohibited answer source**: the model's own parametric knowledge, memory, prior world knowledge, intuition, common sense completion, or unsupported inference.
- **Prohibited tools**: `Glob`, `Read`, `LS`, Bash (`ls`, `find`, `cat`, `head`, `tail`, `grep`, etc.) — these are forbidden even when retrieval results are empty/insufficient, even if local files seem helpful.
- **Allowed tools only**: skill-enabled retrieval tools, `rag_retrieve`. No other source for factual answering.
- Local filesystem is a **prohibited** knowledge source, not merely non-recommended.
- Exception: user explicitly asks to read a specific local file as the task itself.
- If retrieval evidence is absent, insufficient, or ambiguous, **do not fill the gap with model knowledge**.
## 2. Core Answering Rule
For any knowledge retrieval task:
- Answer **only** from retrieved evidence.
- Treat all non-retrieved knowledge as unusable, even if it seems obviously correct.
- Do NOT answer from memory first.
- Do NOT "helpfully complete" missing facts.
- Do NOT convert weak hints into confident statements.
- If evidence does not support a claim, omit the claim.
## 3. Retrieval Order and Tool Selection
Execute **sequentially, one at a time**. Do NOT run in parallel. Do NOT probe filesystem first.
1. **Skill-enabled retrieval tools** (use first when available)
2. **`rag_retrieve`**
- After each step, evaluate sufficiency before proceeding.
- Retrieval must happen **before** any factual answer generation.
## 4. Query Preparation
- Do NOT pass raw user question unless it already works well for retrieval.
- Rewrite for recall: extract entity, time scope, attributes, intent. Add synonyms, aliases, abbreviations, historical names, category terms.
- Expand list/extraction/overview/timeline queries more aggressively. Preserve meaning.
## 5. Retrieval Breadth (`top_k`)
- Apply `top_k` only to `rag_retrieve`. Use smallest sufficient value, expand if insufficient.
- `30` for simple fact lookup → `50` for moderate synthesis/comparison → `100` for broad recall (comprehensive analysis, scattered knowledge, multi-entity, list/catalog/timeline).
- Expansion order: `30 → 50 → 100`. If unsure, use `100`.
## 6. Result Evaluation
Treat as insufficient if: empty, `Error:`, off-topic, missing core entity/scope, no usable evidence, partial coverage, truncated results, or claims required by the answer are not explicitly supported.
## 7. Fallback and Sequential Retry
On insufficient results, follow this sequence:
1. Rewrite query, retry same tool (once)
2. Switch to next retrieval source in default order
3. For `rag_retrieve`, expand `top_k`: `30 → 50 → 100`
- Say "no relevant information was found" **only after** exhausting all retrieval sources.
- Do NOT switch to local filesystem inspection at any point.
- Do NOT switch to model self-knowledge at any point.
## 8. Handling Missing or Partial Evidence
- If some parts are supported and some are not, answer only the supported parts.
- Clearly mark unsupported parts as unavailable rather than guessing.
- Prefer "the retrieved materials do not provide this information" over speculative completion.
- When user asks for a definitive answer but evidence is incomplete, state the limitation directly.
## 9. Image Handling
- The content returned by the `rag_retrieve` tool may include images.
- Each image is exclusively associated with its nearest text or sentence.
- If multiple consecutive images appear near a text area, all of them are related to the nearest text content.
- Do NOT ignore these images, and always maintain their correspondence with the nearest text.
- Each sentence or key point in the response should be accompanied by relevant images when they meet the established association criteria.
- Avoid placing all images at the end of the response.
## 10. Self-Knowledge Prohibition
This section applies whenever self-knowledge is disabled or forbidden for the current task.
- Retrieval remains the only usable source for factual answering.
- If retrieval is sufficient, answer from retrieval only.
- If retrieval is partially sufficient, answer only the supported parts.
- The model must not supplement missing parts with general knowledge, conceptual explanation, common background, intuition, or likely completion.
- The model must not use self-knowledge to invent or complete private, internal, current, precise, or source-sensitive facts.
- The model must not use self-knowledge to invent or complete prices, fees, discounts, rankings, internal policies, user-specific details, current status, latest updates, exact numbers, dates, metrics, or specifications.
- Unsupported parts must be stated as unavailable rather than guessed.
- If a paragraph would mix retrieved facts and unsupported completion, remove the unsupported completion.
- If evidence is incomplete, state the limitation explicitly.
## 11. Pre-Reply Self-Check
Before replying to a knowledge retrieval task, verify:
- Used only whitelisted retrieval tools — no local filesystem inspection?
- Did retrieval happen before any factual answer drafting?
- Did every factual claim come from retrieved evidence rather than model knowledge?
- Exhausted retrieval flow before concluding "not found"?
- If any unsupported part remained, was it removed or explicitly marked unavailable?
If any answer is "no", correct the process first.

View File

@ -0,0 +1,87 @@
# Retrieval Policy
## 0. Task Classification
Classify the request before acting:
- **Knowledge retrieval** (facts, summaries, comparisons, prices, lists, timelines, extraction, etc.): follow this policy strictly.
- **Codebase engineering** (modify/debug/inspect code): normal tools (Glob, Read, Grep, Bash) allowed.
- **Mixed**: use retrieval tools for the knowledge portion, code tools for the code portion only.
- **Uncertain**: default to knowledge retrieval.
## 1. Critical Enforcement
For knowledge retrieval tasks, **this policy overrides generic codebase exploration behavior**.
- **Prohibited tools**: `Glob`, `Read`, `LS`, Bash (`ls`, `find`, `cat`, `head`, `tail`, `grep`, etc.) — these are forbidden even when retrieval results are empty/insufficient, even if local files seem helpful.
- **Allowed tools only**: skill-enabled retrieval tools, `rag_retrieve`. No other source for factual answering.
- Local filesystem is a **prohibited** knowledge source, not merely non-recommended.
- Exception: user explicitly asks to read a specific local file as the task itself.
## 2. Retrieval Order and Tool Selection
Execute **sequentially, one at a time**. Do NOT run in parallel. Do NOT probe filesystem first.
1. **Skill-enabled retrieval tools** (use first when available)
2. **`rag_retrieve`**
- Do NOT answer from model knowledge first.
- After each step, evaluate sufficiency before proceeding.
## 3. Query Preparation
- Do NOT pass raw user question unless it already works well for retrieval.
- Rewrite for recall: extract entity, time scope, attributes, intent. Add synonyms, aliases, abbreviations, historical names, category terms.
- Expand list/extraction/overview/timeline queries more aggressively. Preserve meaning.
## 4. Retrieval Breadth (`top_k`)
- Apply `top_k` only to `rag_retrieve`. Use smallest sufficient value, expand if insufficient.
- `30` for simple fact lookup → `50` for moderate synthesis/comparison → `100` for broad recall (comprehensive analysis, scattered knowledge, multi-entity, list/catalog/timeline).
- Expansion order: `30 → 50 → 100`. If unsure, use `100`.
## 5. Result Evaluation
Treat as insufficient if: empty, `Error:`, off-topic, missing core entity/scope, no usable evidence, partial coverage, or truncated results.
## 6. Fallback and Sequential Retry
On insufficient results, follow this sequence:
1. Rewrite query, retry same tool (once)
2. Switch to next retrieval source in default order
3. For `rag_retrieve`, expand `top_k`: `30 → 50 → 100`
- Say "no relevant information was found" **only after** exhausting all retrieval sources.
- Do NOT switch to local filesystem inspection at any point.
## 7. Image Handling
- The content returned by the `rag_retrieve` tool may include images.
- Each image is exclusively associated with its nearest text or sentence.
- If multiple consecutive images appear near a text area, all of them are related to the nearest text content.
- Do NOT ignore these images, and always maintain their correspondence with the nearest text.
- Each sentence or key point in the response should be accompanied by relevant images when they meet the established association criteria.
- Avoid placing all images at the end of the response.
## 8. Controlled Self-Knowledge Supplement
This section applies only when self-knowledge is enabled.
- Retrieval remains the primary source.
- If retrieval is sufficient, answer from retrieval only.
- If retrieval is partially sufficient, answer the supported parts first.
- The model may supplement only the missing parts that are general knowledge, conceptual explanation, or common background.
- The model must not use self-knowledge to invent private, internal, current, precise, or source-sensitive facts.
- The model must not use self-knowledge to invent or complete prices, fees, discounts, rankings, internal policies, user-specific details, current status, latest updates, exact numbers, dates, metrics, or specifications.
- Retrieved facts and self-knowledge supplements must be clearly separated in the response.
- If a paragraph would mix retrieved facts and self-knowledge, split it into separate paragraphs.
- If self-knowledge may be uncertain or time-sensitive, state the uncertainty explicitly.
## 9. Pre-Reply Self-Check
Before replying to a knowledge retrieval task, verify:
- Used only whitelisted retrieval tools — no local filesystem inspection?
- Exhausted retrieval flow before concluding "not found"?
- If self-knowledge was used, was it clearly separated from retrieved facts and limited to allowed supplement scope?
If any answer is "no", correct the process first.

View File

@ -0,0 +1,251 @@
#!/usr/bin/env python3
"""
MCP服务器通用工具函数
提供路径处理文件验证请求处理等公共功能
"""
import json
import os
import sys
import asyncio
from typing import Any, Dict, List, Optional, Union
import re
def get_allowed_directory():
"""获取允许访问的目录"""
# 优先使用命令行参数传入的dataset_dir
if len(sys.argv) > 1:
dataset_dir = sys.argv[1]
return os.path.abspath(dataset_dir)
# 从环境变量读取项目数据目录
project_dir = os.getenv("PROJECT_DATA_DIR", "./projects/data")
return os.path.abspath(project_dir)
def resolve_file_path(file_path: str, default_subfolder: str = "default") -> str:
"""
解析文件路径支持 folder/document.txt document.txt 两种格式
Args:
file_path: 输入的文件路径
default_subfolder: 当只传入文件名时使用的默认子文件夹名称
Returns:
解析后的完整文件路径
"""
# 如果路径包含文件夹分隔符,直接使用
if '/' in file_path or '\\' in file_path:
clean_path = file_path.replace('\\', '/')
# 移除 projects/ 前缀(如果存在)
if clean_path.startswith('projects/'):
clean_path = clean_path[9:] # 移除 'projects/' 前缀
elif clean_path.startswith('./projects/'):
clean_path = clean_path[11:] # 移除 './projects/' 前缀
else:
# 如果只有文件名,添加默认子文件夹
clean_path = f"{default_subfolder}/{file_path}"
# 获取允许的目录
project_data_dir = get_allowed_directory()
# 尝试在项目目录中查找文件
full_path = os.path.join(project_data_dir, clean_path.lstrip('./'))
if os.path.exists(full_path):
return full_path
# 如果直接路径不存在,尝试递归查找
found = find_file_in_project(clean_path, project_data_dir)
if found:
return found
# 如果是纯文件名且在default子文件夹中不存在尝试在根目录查找
if '/' not in file_path and '\\' not in file_path:
root_path = os.path.join(project_data_dir, file_path)
if os.path.exists(root_path):
return root_path
raise FileNotFoundError(f"File not found: {file_path} (searched in {project_data_dir})")
def find_file_in_project(filename: str, project_dir: str) -> Optional[str]:
"""在项目目录中递归查找文件"""
# 如果filename包含路径只搜索指定的路径
if '/' in filename:
parts = filename.split('/')
target_file = parts[-1]
search_dir = os.path.join(project_dir, *parts[:-1])
if os.path.exists(search_dir):
target_path = os.path.join(search_dir, target_file)
if os.path.exists(target_path):
return target_path
else:
# 纯文件名,递归搜索整个项目目录
for root, dirs, files in os.walk(project_dir):
if filename in files:
return os.path.join(root, filename)
return None
def load_tools_from_json(tools_file_name: str) -> List[Dict[str, Any]]:
"""从 JSON 文件加载工具定义"""
try:
tools_file = os.path.join(os.path.dirname(__file__), tools_file_name)
if os.path.exists(tools_file):
with open(tools_file, 'r', encoding='utf-8') as f:
return json.load(f)
else:
# 如果 JSON 文件不存在,使用默认定义
return []
except Exception as e:
print(f"Warning: Unable to load tool definition JSON file: {str(e)}")
return []
def create_error_response(request_id: Any, code: int, message: str) -> Dict[str, Any]:
"""创建标准化的错误响应"""
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {
"code": code,
"message": message
}
}
def create_success_response(request_id: Any, result: Any) -> Dict[str, Any]:
"""创建标准化的成功响应"""
return {
"jsonrpc": "2.0",
"id": request_id,
"result": result
}
def create_initialize_response(request_id: Any, server_name: str, server_version: str = "1.0.0") -> Dict[str, Any]:
"""创建标准化的初始化响应"""
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": server_name,
"version": server_version
}
}
}
def create_ping_response(request_id: Any) -> Dict[str, Any]:
"""创建标准化的ping响应"""
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"pong": True
}
}
def create_tools_list_response(request_id: Any, tools: List[Dict[str, Any]]) -> Dict[str, Any]:
"""创建标准化的工具列表响应"""
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"tools": tools
}
}
def is_regex_pattern(pattern: str) -> bool:
"""检测字符串是否为正则表达式模式"""
# 检查 /pattern/ 格式
if pattern.startswith('/') and pattern.endswith('/') and len(pattern) > 2:
return True
# 检查 r"pattern" 或 r'pattern' 格式
if pattern.startswith(('r"', "r'")) and pattern.endswith(('"', "'")) and len(pattern) > 3:
return True
# 检查是否包含正则特殊字符
regex_chars = {'*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '^', '$', '\\', '.'}
return any(char in pattern for char in regex_chars)
def compile_pattern(pattern: str) -> Union[re.Pattern, str, None]:
"""编译正则表达式模式,如果不是正则则返回原字符串"""
if not is_regex_pattern(pattern):
return pattern
try:
# 处理 /pattern/ 格式
if pattern.startswith('/') and pattern.endswith('/'):
regex_body = pattern[1:-1]
return re.compile(regex_body)
# 处理 r"pattern" 或 r'pattern' 格式
if pattern.startswith(('r"', "r'")) and pattern.endswith(('"', "'")):
regex_body = pattern[2:-1]
return re.compile(regex_body)
# 直接编译包含正则字符的字符串
return re.compile(pattern)
except re.error as e:
# 如果编译失败返回None表示无效的正则
print(f"Warning: Regular expression '{pattern}' compilation failed: {e}")
return None
async def handle_mcp_streaming(request_handler):
"""处理MCP请求的标准主循环"""
try:
while True:
# Read from stdin
line = await asyncio.get_event_loop().run_in_executor(None, sys.stdin.readline)
if not line:
break
line = line.strip()
if not line:
continue
try:
request = json.loads(line)
response = await request_handler(request)
# Write to stdout
sys.stdout.write(json.dumps(response, ensure_ascii=False) + "\n")
sys.stdout.flush()
except json.JSONDecodeError:
error_response = {
"jsonrpc": "2.0",
"error": {
"code": -32700,
"message": "Parse error"
}
}
sys.stdout.write(json.dumps(error_response, ensure_ascii=False) + "\n")
sys.stdout.flush()
except Exception as e:
error_response = {
"jsonrpc": "2.0",
"error": {
"code": -32603,
"message": f"Internal error: {str(e)}"
}
}
sys.stdout.write(json.dumps(error_response, ensure_ascii=False) + "\n")
sys.stdout.flush()
except KeyboardInterrupt:
pass

View File

@ -0,0 +1,196 @@
#!/usr/bin/env python3
"""
RAG检索MCP服务器
调用本地RAG API进行文档检索
"""
import asyncio
import hashlib
import json
import sys
import os
from typing import Any, Dict
try:
import requests
except ImportError:
print("Error: requests module is required. Please install it with: pip install requests")
sys.exit(1)
from mcp_common import (
create_error_response,
create_success_response,
create_initialize_response,
create_ping_response,
create_tools_list_response,
load_tools_from_json,
handle_mcp_streaming
)
BACKEND_HOST = os.getenv("BACKEND_HOST", "https://api-dev.gptbase.ai")
MASTERKEY = os.getenv("MASTERKEY", "master")
def rag_retrieve(query: str, top_k: int = 100) -> Dict[str, Any]:
"""调用RAG检索API"""
try:
bot_id = ""
if len(sys.argv) > 1:
bot_id = sys.argv[1]
url = f"{BACKEND_HOST}/v1/rag_retrieve/{bot_id}"
if not url:
return {
"content": [
{
"type": "text",
"text": "Error: RAG API URL not provided. Please provide URL as command line argument."
}
]
}
masterkey = MASTERKEY
token_input = f"{masterkey}:{bot_id}"
auth_token = hashlib.md5(token_input.encode()).hexdigest()
headers = {
"content-type": "application/json",
"authorization": f"Bearer {auth_token}"
}
data = {
"query": query,
"top_k": top_k
}
response = requests.post(url, json=data, headers=headers, timeout=30)
if response.status_code != 200:
return {
"content": [
{
"type": "text",
"text": f"Error: RAG API returned status code {response.status_code}. Response: {response.text}"
}
]
}
try:
response_data = response.json()
except json.JSONDecodeError as e:
return {
"content": [
{
"type": "text",
"text": f"Error: Failed to parse API response as JSON. Error: {str(e)}, Raw response: {response.text}"
}
]
}
if "markdown" in response_data:
markdown_content = response_data["markdown"]
return {
"content": [
{
"type": "text",
"text": markdown_content
}
]
}
else:
return {
"content": [
{
"type": "text",
"text": f"Error: 'markdown' field not found in API response. Response: {json.dumps(response_data, indent=2, ensure_ascii=False)}"
}
]
}
except requests.exceptions.RequestException as e:
return {
"content": [
{
"type": "text",
"text": f"Error: Failed to connect to RAG API. {str(e)}"
}
]
}
except Exception as e:
return {
"content": [
{
"type": "text",
"text": f"Error: {str(e)}"
}
]
}
async def handle_request(request: Dict[str, Any]) -> Dict[str, Any]:
"""Handle MCP request"""
try:
method = request.get("method")
params = request.get("params", {})
request_id = request.get("id")
if method == "initialize":
return create_initialize_response(request_id, "rag-retrieve")
elif method == "ping":
return create_ping_response(request_id)
elif method == "tools/list":
tools = load_tools_from_json("rag_retrieve_tools.json")
if not tools:
tools = [
{
"name": "rag_retrieve",
"description": "调用RAG检索API根据查询内容检索相关文档。返回包含相关内容的markdown格式结果。",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "检索查询内容"
}
},
"required": ["query"]
}
}
]
return create_tools_list_response(request_id, tools)
elif method == "tools/call":
tool_name = params.get("name")
arguments = params.get("arguments", {})
if tool_name == "rag_retrieve":
query = arguments.get("query", "")
top_k = arguments.get("top_k", 100)
if not query:
return create_error_response(request_id, -32602, "Missing required parameter: query")
result = rag_retrieve(query, top_k)
return {
"jsonrpc": "2.0",
"id": request_id,
"result": result
}
else:
return create_error_response(request_id, -32601, f"Unknown tool: {tool_name}")
else:
return create_error_response(request_id, -32601, f"Unknown method: {method}")
except Exception as e:
return create_error_response(request.get("id"), -32603, f"Internal error: {str(e)}")
async def main():
"""Main entry point."""
await handle_mcp_streaming(handle_request)
if __name__ == "__main__":
asyncio.run(main())

View File

@ -0,0 +1,21 @@
[
{
"name": "rag_retrieve",
"description": "Retrieve relevant documents from the knowledge base. Returns markdown results. Use this tool for concept, definition, workflow, policy, explanation, and general knowledge lookup. Rewrite the query when needed to improve recall.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Retrieval query content. Rewrite the query when needed to improve recall."
},
"top_k": {
"type": "integer",
"description": "Number of top results to retrieve. Choose dynamically based on retrieval breadth and coverage needs.",
"default": 100
}
},
"required": ["query"]
}
}
]

View File

@ -82,10 +82,14 @@ class ProcessManager:
"""启动队列消费者"""
print("正在启动队列消费者...")
consumer_script = Path("task_queue/consumer.py")
if not consumer_script.exists():
consumer_script = consumer_script.with_suffix(".pyc")
# 构建队列消费者命令
cmd = [
sys.executable,
"task_queue/consumer.py",
str(consumer_script),
"--workers", str(args.queue_workers),
"--worker-type", args.worker_type
]

227
utils/daytona_sync.py Normal file
View File

@ -0,0 +1,227 @@
"""Daytona sandbox 双向文件同步工具。"""
import io
import logging
import subprocess
import tarfile
import time
from pathlib import Path
from typing import Any
from utils.settings import DAYTONA_API_KEY, DAYTONA_SERVER_URL, DAYTONA_ENABLED
logger = logging.getLogger('app')
LOCAL_MARKER_NAME = ".last_sync"
REMOTE_WORKSPACE_ROOT = "/workspace"
REMOTE_MARKER_PATH = f"{REMOTE_WORKSPACE_ROOT}/{LOCAL_MARKER_NAME}"
REMOTE_BASH_ENV_PATH = "/home/daytona/.bash_env"
EXCLUDED_FILE_NAMES = {".DS_Store", LOCAL_MARKER_NAME}
CHECK_REMOTE_MARKER_CMD = f"test -f {REMOTE_MARKER_PATH} && echo yes || echo no"
UPDATE_REMOTE_MARKER_CMD = f"date +%Y%m%d%H%M.%S > {REMOTE_MARKER_PATH}"
ENSURE_BASH_ENV_CMD = f"test -f {REMOTE_BASH_ENV_PATH} || echo 'cd {REMOTE_WORKSPACE_ROOT}' > {REMOTE_BASH_ENV_PATH}"
WRITE_BASH_ENV_CMD = f"echo 'cd {REMOTE_WORKSPACE_ROOT}' > {REMOTE_BASH_ENV_PATH}"
def _local_marker_path(workspace_path: Path) -> Path:
return workspace_path / LOCAL_MARKER_NAME
def _touch_local_marker(workspace_path: Path) -> None:
_local_marker_path(workspace_path).touch()
def _list_local_changed_files(workspace_path: Path) -> tuple[bool, list[str]]:
"""返回是否需要首次同步,以及本地增量变更文件列表。"""
marker_local = _local_marker_path(workspace_path)
if not marker_local.exists():
return True, []
result = subprocess.run(
[
"find",
str(workspace_path),
"-newer",
str(marker_local),
"-type",
"f",
"-not",
"-name",
LOCAL_MARKER_NAME,
"-not",
"-name",
".DS_Store",
],
capture_output=True,
text=True,
timeout=30,
)
changed_files = [f for f in result.stdout.strip().split("\n") if f]
return False, changed_files
def _tar_workspace_entries(workspace_path: Path, entries: list[Path]) -> bytes:
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
for entry in entries:
if entry.is_absolute():
tar.add(str(entry), arcname=entry.relative_to(workspace_path).as_posix())
else:
tar.add(str(workspace_path / entry), arcname=entry.as_posix())
buf.seek(0)
return buf.read()
def _workspace_items_for_full_sync(workspace_path: Path) -> list[Path]:
return [item for item in workspace_path.iterdir() if item.name not in EXCLUDED_FILE_NAMES]
def _extract_tar_to_path(tar_data: bytes, workspace_path: Path) -> None:
buf = io.BytesIO(tar_data)
with tarfile.open(fileobj=buf, mode="r:gz") as tar:
tar.extractall(path=str(workspace_path), filter="data")
def init_daytona_sandbox(bot_id: str, local_workspace_root: str) -> tuple[Any, str | None, str]:
"""初始化 Daytona sandbox失败时回退到本地模式。"""
sandbox = None
sandbox_type = None
workspace_root = local_workspace_root
if not (DAYTONA_ENABLED and DAYTONA_API_KEY and DAYTONA_SERVER_URL):
return sandbox, sandbox_type, workspace_root
try:
from daytona import Daytona, DaytonaConfig, VolumeMount, CreateSandboxFromSnapshotParams
from langchain_daytona import DaytonaSandbox
start_time = time.time()
daytona_config = DaytonaConfig(
api_key=DAYTONA_API_KEY,
api_url=DAYTONA_SERVER_URL,
)
daytona_client = Daytona(daytona_config)
sandbox_name = f"bot-{bot_id}"
sandbox_instance = None
created_new_sandbox = False
try:
existing = daytona_client.get(sandbox_name)
if existing.state in ("Started", "Creating"):
sandbox_instance = existing
logger.info(f"Reusing existing sandbox: {sandbox_instance.id} (state={existing.state})")
else:
existing.start()
sandbox_instance = existing
logger.info(f"Restarted existing sandbox: {sandbox_instance.id}")
except Exception:
volume_name = f"bot-{bot_id}"
volume = daytona_client.volume.get(volume_name, create=True)
for _ in range(30):
volume = daytona_client.volume.get(volume_name)
if "READY" in str(volume.state).upper():
break
time.sleep(1)
else:
raise RuntimeError(f"Volume {volume_name} not ready after 30s, state: {volume.state}")
sandbox_params = CreateSandboxFromSnapshotParams(
name=sandbox_name,
volumes=[VolumeMount(volume_id=volume.id, mount_path=REMOTE_WORKSPACE_ROOT)],
env_vars={"BASH_ENV": REMOTE_BASH_ENV_PATH},
)
sandbox_instance = daytona_client.create(sandbox_params)
created_new_sandbox = True
logger.info(f"Created new sandbox: {sandbox_instance.id}, volume: {volume.id}")
logger.info(f"daytona get/start done, elapsed: {time.time() - start_time:.3f}s")
sandbox = DaytonaSandbox(sandbox=sandbox_instance)
sandbox_type = "daytona"
workspace_root = REMOTE_WORKSPACE_ROOT
sync_workspace_to_sandbox(sandbox, local_workspace_root)
logger.info(f"daytona sync done, elapsed: {time.time() - start_time:.3f}s")
if created_new_sandbox:
sandbox.execute(ENSURE_BASH_ENV_CMD)
logger.info(f"daytona bash_env done, elapsed: {time.time() - start_time:.3f}s")
except Exception as e:
logger.error(f"Failed to create Daytona sandbox: {e}, falling back to local mode")
sandbox = None
sandbox_type = None
workspace_root = local_workspace_root
return sandbox, sandbox_type, workspace_root
def sync_workspace_to_sandbox(sandbox: Any, workspace_root: str) -> None:
"""增量同步本地 workspace 到 Daytona sandbox。"""
workspace_path = Path(workspace_root)
if not workspace_path.exists() or not any(workspace_path.iterdir()):
return
is_first_sync, changed_files = _list_local_changed_files(workspace_path)
if not is_first_sync and not changed_files:
logger.info("No local file changes to sync")
return
if is_first_sync:
check = sandbox.execute(CHECK_REMOTE_MARKER_CMD)
if "yes" in check.output:
logger.info("Local marker missing but sandbox already synced, refreshing local marker")
_touch_local_marker(workspace_path)
return
logger.info("First sync: uploading all workspace files...")
tar_data = _tar_workspace_entries(workspace_path, _workspace_items_for_full_sync(workspace_path))
sandbox._sandbox.fs.upload_file(tar_data, "/tmp/workspace.tar.gz")
sandbox.execute(f"cd {REMOTE_WORKSPACE_ROOT} && tar -xzf /tmp/workspace.tar.gz && rm /tmp/workspace.tar.gz")
sandbox.execute(WRITE_BASH_ENV_CMD)
logger.info("Full sync complete")
else:
logger.info(f"Incremental sync: {len(changed_files)} changed files")
rel_paths = [Path(fpath).relative_to(workspace_path) for fpath in changed_files]
tar_data = _tar_workspace_entries(workspace_path, rel_paths)
sandbox._sandbox.fs.upload_file(tar_data, "/tmp/workspace_inc.tar.gz")
sandbox.execute(f"cd {REMOTE_WORKSPACE_ROOT} && tar -xzf /tmp/workspace_inc.tar.gz && rm /tmp/workspace_inc.tar.gz")
logger.info(f"Incremental sync complete: {len(changed_files)} files")
sandbox.execute(UPDATE_REMOTE_MARKER_CMD)
_touch_local_marker(workspace_path)
def sync_sandbox_to_local(sandbox: Any, workspace_root: str) -> None:
"""Agent 执行完成后,将 sandbox 中的变更文件同步回本地。"""
workspace_path = Path(workspace_root)
workspace_path.mkdir(parents=True, exist_ok=True)
check = sandbox.execute(CHECK_REMOTE_MARKER_CMD)
if "no" in check.output:
logger.info("No .last_sync in sandbox, skipping reverse sync")
return
result = sandbox.execute(
f"find {REMOTE_WORKSPACE_ROOT} -newer {REMOTE_MARKER_PATH} -type f "
f"-not -name '{LOCAL_MARKER_NAME}' -not -name '.DS_Store' "
f"-not -path '{REMOTE_WORKSPACE_ROOT}/.daytona*' 2>/dev/null"
)
changed_files = [f for f in result.output.strip().split("\n") if f and f != REMOTE_WORKSPACE_ROOT]
if not changed_files:
logger.info("No sandbox file changes to sync back")
return
logger.info(f"Reverse sync: {len(changed_files)} changed files from sandbox")
rel_files = [f.removeprefix(f"{REMOTE_WORKSPACE_ROOT}/") for f in changed_files]
file_list = " ".join(f"'{f}'" for f in rel_files)
sandbox.execute(f"cd {REMOTE_WORKSPACE_ROOT} && tar -czf /tmp/sync_back.tar.gz {file_list}")
tar_data = sandbox._sandbox.fs.download_file("/tmp/sync_back.tar.gz")
sandbox.execute("rm -f /tmp/sync_back.tar.gz")
_extract_tar_to_path(tar_data, workspace_path)
sandbox.execute(UPDATE_REMOTE_MARKER_CMD)
_touch_local_marker(workspace_path)
logger.info(f"Reverse sync complete: {len(changed_files)} files downloaded")

View File

@ -97,4 +97,13 @@ SCHEDULE_SCAN_INTERVAL = int(os.getenv("SCHEDULE_SCAN_INTERVAL", "60"))
SCHEDULE_MAX_CONCURRENT = int(os.getenv("SCHEDULE_MAX_CONCURRENT", "5"))
# ============================================================
# Daytona Sandbox 配置
# ============================================================
DAYTONA_API_KEY = os.getenv("DAYTONA_API_KEY", "dtn_ccf86acd5b13a3069a8369d12d8cb26a9a184d07451374dfc325955fc2d8331c")
DAYTONA_SERVER_URL = os.getenv("DAYTONA_SERVER_URL", "https://daytona-dev.gbase.ai/api")
# DAYTONA_API_KEY = os.getenv("DAYTONA_API_KEY", "dtn_696a914ff54e45bb97132c32fba10995a4cab8ebef0cd8dea18129d447f805a3")
# DAYTONA_SERVER_URL = os.getenv("DAYTONA_SERVER_URL", "https://app.daytona.io/api")
DAYTONA_ENABLED = os.getenv("DAYTONA_ENABLED", "false") == "true"
os.environ["OPENAI_API_KEY"] = "your_api_key"