Merge branch 'dev' into staging
This commit is contained in:
commit
100007f66b
22
Dockerfile
22
Dockerfile
@ -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}"]
|
||||
|
||||
@ -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}"]
|
||||
|
||||
@ -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
|
||||
|
||||
# 并行执行高耗时 IO:MCP 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()
|
||||
|
||||
# 从连接池获取 checkpointer(prepare_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(
|
||||
|
||||
@ -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
461
poetry.lock
generated
@ -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"
|
||||
|
||||
@ -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. 更新设备(此操作需要确认)
|
||||
- **条件**:用户意图为控制设备或调节参数(如开关、温度、风速), 需要进行确认。
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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:
|
||||
"""
|
||||
|
||||
@ -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",
|
||||
|
||||
12
skills/developing/novare-context/.claude-plugin/plugin.json
Normal file
12
skills/developing/novare-context/.claude-plugin/plugin.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "novare-context",
|
||||
"description": "NOVAREの現在のユーザー詳細情報を自動的に読み込みます",
|
||||
"hooks": {
|
||||
"PrePrompt": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python hooks/pre_prompt.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
153
skills/developing/novare-context/README.md
Normal file
153
skills/developing/novare-context/README.md
Normal 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**: 敏感信息脱敏后再存储,如手机号、邮箱等
|
||||
166
skills/developing/novare-context/hooks/pre_prompt.py
Normal file
166
skills/developing/novare-context/hooks/pre_prompt.py
Normal 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())
|
||||
@ -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}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
34
skills/developing/rag-retrieve-no-citation/README.md
Normal file
34
skills/developing/rag-retrieve-no-citation/README.md
Normal 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`
|
||||
@ -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())
|
||||
@ -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.
|
||||
@ -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.
|
||||
251
skills/developing/rag-retrieve-no-citation/mcp_common.py
Normal file
251
skills/developing/rag-retrieve-no-citation/mcp_common.py
Normal 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
|
||||
@ -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())
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -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
227
utils/daytona_sync.py
Normal 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")
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user