mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-09-05 08:06:15 +00:00
### 1. Agent Wrapper(统一 Agent 后端抽象) - **`base_agent_wrapper.py`**:`reply()` 返回值从 `tuple[str, Any]` 改为 `dict`(含 `session_id` / `last_message` / `result` / 可选 `structured_output`);`reply_stream()` 改为产出统一的 `StreamChunk`。废弃 `add_tools()`,改为 `add_job_tools(names: list[str])`(按名解析 BaseJob)与 `add_skills()`;新增 `_resolve_job_tools()`、`_merged_kwargs()`、`_chunk()` 辅助方法及 `project_path` / `project_skills_root` 属性。 - **`as_agent_wrapper.py`(AgentScope 后端)**: - 会话持久化重写:`session_path` 落地到 `<vault>/<session_dir>/agentscope/`,`_load_state` 支持 `resume` / `session_id` / `fork_session`,并做 UUID 校验(`_validate_session_id`);`_cleanup_expired_sessions` 按天数清理过期会话。 - 新增内置工具集(`BypassAnalysisBash` + Edit/Glob/Grep/Read/Write),`BypassAnalysisBash` 绕过 AgentScope 自带 Bash 静态分析以让 permission_mode 生效;`_resolve_skills()` 把配置的 skill 暴露给后端,`_load_tool_env()` 注入项目 `.env`。 - `_event_to_chunk()` 把 20+ 种 AgentScope 事件(Reply/Text/Thinking/Data/ToolCall/ToolResult/ModelCall/ExceedMaxIters)归一化为 `StreamChunk`。 - **`cc_agent_wrapper.py`(Claude Code SDK 后端,+551 行)**: - 新增 `_CcFileSessionStore`:基于 vault 的文件型会话存储,实现 append(按 uuid 去重)/ load / list / delete / list_subkeys,并对路径做 `_safe_parts` + `resolve()` 防越界校验。 - `_build_options()`:统一构建 `ClaudeAgentOptions`,处理 skills、disallowed_tools(默认禁 `WebSearch`)、`.env` 注入、Claude Code 的 API 凭据解析(`_claude_code_api_env`,多级 base_url/api_key 回退)、`CLAUDE_CONFIG_DIR` 设置、skill 目录软链接(`_ensure_claude_skill_dir`)。 - `_raw_event_to_chunk()` / `_message_content_to_chunks()`:把 Anthropic 流式事件(message_start/delta/stop、content_block_*)与 SDK 消息块(AssistantMessage/UserMessage/ResultMessage/RateLimitEvent)转换为统一 `StreamChunk`;跟踪 block_id/block_type/tool_call_name 做关联;处理尾部 `"success"` 误报异常的吞掉逻辑。 ### 2. 统一流式协议(StreamChunk / ChunkEnum) - **`stream_chunk.py`**:`StreamChunk` 扩展为承载 AS + CC 双后端完整信息的统一结构,新增 `session_id` / `block_id` / `tool_call_id` / `tool_call_name` / `media_type` / `input_tokens` / `output_tokens` 等字段,纯文本流仍保持轻量。 - **`chunk_enum.py`**:补全生命周期标记 `REPLY_START` / `REPLY_END`,并文档化两套后端事件 → ChunkEnum 的映射。 ### 3. Index 模块重构(变化批次化 + dispatch) - 新增 `_change_batch.py`:`coalesce_changes()` 把同路径多次事件折叠为最终状态(结合 path 存在性判定),`bucket_changes()` 按 watchfiles.Change 分桶。 - 新增 `init_changes.py`(`InitChangesStep`):一次性扫描,对比 file_store / file_catalog 已索引节点计算 added/modified/deleted,写入 `context["changes"]` 后 dispatch。 - 新增 `update_changes.py`:抽象基类 `ChangeApplyStep` 统一 added/modified/deleted 处理与错误收集;`UpdateCatalogStep`(写 file_catalog)、`UpdateIndexStep`(写 file_store,含按后缀解析 chunker)。 - **`watch_changes.py`**:改用 `dispatch_step_specs`(基类提供的 `dispatch_steps()`),每批先 `coalesce_changes` 再 dispatch;默认参数调整(debounce 5000ms / step 1000ms / poll 5000ms)并暴露常量。 - 删除旧步骤:`clear_and_scan` / `foreach_dispatch` / `scan_changes` / `update_catalog`(旧) / `update_index`(旧);`clear_store.py` 取代 clear_and_scan。 ### 4. Evolve / Dream 模块(拆分为多步 pipeline) - 删除旧的单体 `auto_dream.py` / `dream.py` / `dream.yaml`,新增 `dream/` 子包,按 5 个步骤组织: - **`extract.py`**:扫描当日 day-index + daily 笔记,对比 file_catalog 找出 changed/deleted,调用 LLM 全局抽取 `units`(procedure/personal/wiki 三桶)与 `topics`,路径与桶做清洗/路由。 - **`integrate.py`**:逐个 unit 调用 LLM 写入 digest,结构化输出 `IntegrateOutcome`(CREATE/CORROBORATE/REFINE/CORRECT),失败 unit/路径收集回写。 - **`topics.py`**:写 `daily/<date>/interests.yaml`,结合当天已有 + 近 N 天做去重(`normalize_topic`),可走 LLM 或纯规则去重两条路径。 - **`proactive.py`**:读取当日 `interests.yaml`,作为主动推荐话题的入口。 - **`finish.py`**:把变更路径落盘到 dream file_catalog(checkpoint),渲染最终汇总摘要。 - 新增 `schema.py`(`DreamState` 等跨步骤共享状态与结构化输出模型)与 `utils.py`(状态存取、扫描打包、YAML 读写、结构化回复解析等公共函数)。 - `evolve/__init__.py` 导出全部新 step。 ### 5. auto_memory / auto_resource(适配新 Agent API) - **`auto_memory.py`**:会话路径迁移到 `<session_dir>/dialog/<session_id>.jsonl`;改用 `job_tools`;新增 `source_conversation` frontmatter 反向链接(`_session_link`);执行后刷新 day 索引(`refresh_day_index`),并对 session_id 做合法性校验。 - **`auto_resource.py`**:资源改用「同名 daily note」方案(`_compute_note_stem` 取文件 stem);批量处理 `changes: list[dict]`(`_handle_change` 逐项处理,返回逐项结果摘要);agent 会话 id 用稳定的 `uuid5`;同样刷新 day 索引。 ### 6. BaseStep 基类增强 - 新增 `dispatch_steps` / `dispatch_step_specs` 机制:`_resolve_dispatch_step()` 支持字符串或 dict 形式的 step spec,`dispatch_steps()` 复用当前 context 调用下游 step。 - 新增 `config_value()`:按 key 取 app config,缺失时回退 `ApplicationConfig` 默认值。 - 小幅清理:`language` 初始化、`copy()`、`Ref.__init__` 签名精简。 ### 7. Components 改动 - **`file_store/local_file_store.py`**:持久化改用 zstd 压缩(`.jsonl.zst`,通过新 `utils/jsonl_zst.py`);upsert 时先删除旧 chunk 的 keyword 文档;embedding 复用改为 `(text, embedding)` 键控,要求文本一致才复用;新增 `_matches_search_filter()` 对 vector/keyword 搜索做 path/path_prefix/metadata 的统一后过滤。 - **`keyword_index/bm25_index.py`**:索引文件名加入组件名 + tokenizer 指纹(sha256 前 12 位),快照/恢复时校验指纹防配置漂移;空索引 dump 时删除文件,加载失败抛错而非静默。 - **`file_chunker/markdown_file_chunker.py`**:弃用 `python-frontmatter`,改用内置 YAML 解析(非法 YAML 不阻断正文索引),并修正因 frontmatter 占用行号导致的 AST 行号偏移(`line_offset`)。 - **`cron_job.py`**:大幅简化(-187 行),由原来「dispatch 外部 job/step + 多种调度模式」改为「在自身 steps 上跑 cron 表达式」;`Application` 启动顺序随之调整为 base > stream > background > cron。 - 其余小调整:service(base/http/mcp)、file_graph、file_catalog、as_llm、as_embedding、tokenizer、prompt_handler、base_component 的签名/接口微调。 ### 8. Application 生命周期 - `_start()` 启动顺序明确为 components → base → stream → background → cron,启动失败会触发 `_close()` 回滚并 re-raise(不再吞异常)。 - 启动时创建 `session_dir` 目录;新增 `update_component()`(按类型/名就地更新已存在组件,不存在则报错)。 ### 9. File IO / 路径安全 - **`_path.py`**:`resolve_path` 增加 vault 越界防护(`is_relative_to` 校验),禁止 `.` / `..` 路径分量,支持 `allow_empty`。 - **`read.py`**:大文件(超过 `MAX_FILE_READ_BYTES`)走按行读取 `read_file_lines_safe`,避免一次性载入内存。 - **`_file_io.py` / `_daily_index.py` / `_path.py`** 等支持函数补齐(如 `refresh_day_index`、`read_file_lines_safe`)。 - **`env_utils.py`**:新增 `parse_env_file()`,`load_env()` 返回加载到的键值、支持 `override`、对无路径调用做幂等缓存。 ### 10. Config - `ApplicationConfig` 新增 `session_dir`(默认 `reme_session`)。 - `config_parser.py`:环境变量展开后做类型转换(`_convert_value`)、dot-notation 与 key=value 参数校验更严格、配置文件路径支持相对 `_CONFIG_DIR` 查找、根非 dict 报错。 - `default.yaml`:作业编排改用 `init_changes_step` + `dispatch_steps`(index/resource/digest 三个 watch loop 与 reindex);新增 `auto_dream`(4 步)、`proactive` 作业,移除旧 `dream`;file_catalog 增配 `resource` / `digest` / `dream` 实例;LLM 默认值与 Claude Code 凭据配置调整(tool_result_limit 50000、thinking_enable=false 等)。 ### 11. 其它 - 新增 `steps/common/add.py`(`AddStep` 算术 demo)、`channel/__init__.py` 与 common `__init__` 导出整理。 - 新增 4 篇文档:`docs4/auto_dream_logic_and_step_refactor.md`、`docs4/watch_loop_step_refactor_plan.md`、`docs4/todo.md`,以及 `reme_design.md` 更新。 **
657 lines
30 KiB
Python
657 lines
30 KiB
Python
"""High-level entry point for configuring and running ReMe services and flows."""
|
|
|
|
import asyncio
|
|
import os
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from pathlib import Path
|
|
|
|
from .embedding import BaseEmbeddingModel
|
|
from .file_store import BaseFileStore
|
|
from .file_watcher import BaseFileWatcher
|
|
from .flow import BaseFlow
|
|
from .llm import BaseLLM
|
|
from .prompt_handler import PromptHandler
|
|
from .registry_factory import R
|
|
from .schema import (
|
|
EmbeddingModelConfig,
|
|
Response,
|
|
ServiceConfig,
|
|
LLMConfig,
|
|
VectorStoreConfig,
|
|
FileStoreConfig,
|
|
FileWatcherConfig,
|
|
TokenCounterConfig,
|
|
)
|
|
from .service_context import ServiceContext
|
|
from .token_counter import BaseTokenCounter
|
|
from .utils import execute_stream_task, PydanticConfigParser, init_logger, MCPClient, print_logo, get_logger, load_env
|
|
from .vector_store import BaseVectorStore
|
|
|
|
logger = get_logger()
|
|
|
|
|
|
class Application:
|
|
"""Application wrapper that wires together service context, flows, and runtimes."""
|
|
|
|
def __init__(
|
|
self,
|
|
*args,
|
|
llm_api_key: str | None = None,
|
|
llm_base_url: str | None = None,
|
|
embedding_api_key: str | None = None,
|
|
embedding_base_url: str | None = None,
|
|
working_dir: str | None = None,
|
|
config_path: str | None = None,
|
|
enable_logo: bool = True,
|
|
log_to_console: bool = True,
|
|
log_to_file: bool = True,
|
|
enable_load_env: bool = True,
|
|
parser: type[PydanticConfigParser] | None = None,
|
|
default_as_llm_config: dict | None = None,
|
|
default_as_llm_formatter_config: dict | None = None,
|
|
default_llm_config: dict | None = None,
|
|
default_embedding_model_config: dict | None = None,
|
|
default_vector_store_config: dict | None = None,
|
|
default_file_store_config: dict | None = None,
|
|
default_token_counter_config: dict | None = None,
|
|
default_file_watcher_config: dict | None = None,
|
|
**kwargs,
|
|
):
|
|
|
|
if enable_load_env:
|
|
load_env()
|
|
|
|
self.llm_api_key = llm_api_key or os.getenv("LLM_API_KEY", "")
|
|
self.llm_base_url = llm_base_url or os.getenv("LLM_BASE_URL", "")
|
|
self.embedding_api_key = embedding_api_key or os.getenv("EMBEDDING_API_KEY", "")
|
|
self.embedding_base_url = embedding_base_url or os.getenv("EMBEDDING_BASE_URL", "")
|
|
|
|
self.service_context = ServiceContext(
|
|
*args,
|
|
service_config=None,
|
|
parser=parser,
|
|
working_dir=working_dir,
|
|
config_path=config_path,
|
|
enable_logo=enable_logo,
|
|
log_to_console=log_to_console,
|
|
log_to_file=log_to_file,
|
|
default_as_llm_config=default_as_llm_config,
|
|
default_as_llm_formatter_config=default_as_llm_formatter_config,
|
|
default_llm_config=default_llm_config,
|
|
default_embedding_model_config=default_embedding_model_config,
|
|
default_vector_store_config=default_vector_store_config,
|
|
default_file_store_config=default_file_store_config,
|
|
default_token_counter_config=default_token_counter_config,
|
|
default_file_watcher_config=default_file_watcher_config,
|
|
**kwargs,
|
|
)
|
|
|
|
self.prompt_handler = PromptHandler(language=self.service_config.language)
|
|
|
|
# NOTE: flows are initialized here to start service!
|
|
self.init_flows()
|
|
|
|
self._started: bool = False
|
|
|
|
@classmethod
|
|
async def create(cls, *args, **kwargs) -> "Application":
|
|
"""Create and start an Application instance asynchronously."""
|
|
instance = cls(*args, **kwargs)
|
|
await instance.start()
|
|
return instance
|
|
|
|
def init_flows(self):
|
|
"""Initialize flows."""
|
|
expression_flow_cls = None
|
|
for name, flow_cls in R.flows.items():
|
|
if not self._filter_flows(name):
|
|
continue
|
|
|
|
if name == "ExpressionFlow":
|
|
expression_flow_cls = flow_cls
|
|
else:
|
|
flow: "BaseFlow" = flow_cls(name=name, service_context=self.service_context)
|
|
self.service_context.flows[flow.name] = flow
|
|
|
|
if expression_flow_cls is not None:
|
|
for name, flow_config in self.service_config.flows.items():
|
|
if not self._filter_flows(name):
|
|
continue
|
|
flow_config.name = name
|
|
flow: BaseFlow = expression_flow_cls( # noqa
|
|
flow_config=flow_config,
|
|
service_context=self.service_context,
|
|
)
|
|
self.service_context.flows[flow.name] = flow
|
|
else:
|
|
logger.info("No expression flow found, please check your configuration.")
|
|
|
|
def _filter_flows(self, name: str) -> bool:
|
|
"""Filter flows based on enabled_flows and disabled_flows configuration."""
|
|
if self.service_config.enabled_flows:
|
|
return name in self.service_config.enabled_flows
|
|
elif self.service_config.disabled_flows:
|
|
return name not in self.service_config.disabled_flows
|
|
else:
|
|
return True
|
|
|
|
@property
|
|
def service_config(self) -> ServiceConfig:
|
|
"""Get the service configuration."""
|
|
return self.service_context.service_config
|
|
|
|
async def start(self):
|
|
"""Start the service context by initializing all configured components."""
|
|
if self._started:
|
|
logger.warning("Application has already started.")
|
|
return self
|
|
|
|
init_logger(
|
|
log_to_console=self.service_config.log_to_console,
|
|
log_to_file=self.service_config.log_to_file,
|
|
)
|
|
logger.info(f"Init ReMe with config: {self.service_config.model_dump_json()}")
|
|
|
|
working_path = Path(self.service_config.working_dir)
|
|
working_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
if self.service_config.ray_max_workers > 1:
|
|
import ray
|
|
|
|
if not ray.is_initialized():
|
|
ray.init(num_cpus=self.service_config.ray_max_workers)
|
|
|
|
if self.service_config.thread_pool_max_workers > 0 and (
|
|
self.service_context.thread_pool is None
|
|
or self.service_context.thread_pool._shutdown # pylint: disable=protected-access
|
|
):
|
|
self.service_context.thread_pool = ThreadPoolExecutor(
|
|
max_workers=self.service_config.thread_pool_max_workers,
|
|
)
|
|
elif self.service_config.thread_pool_max_workers <= 0:
|
|
logger.info("Thread pool is disabled (thread_pool_max_workers <= 0)")
|
|
|
|
if self.service_context.service_config.enable_logo:
|
|
print_logo(service_config=self.service_config)
|
|
|
|
for name, config in self.service_config.as_llms.items():
|
|
if config.backend not in R.as_llms:
|
|
logger.warning(f"AS LLM backend {config.backend} is not supported.")
|
|
else:
|
|
try:
|
|
config_dict = config.model_dump(exclude={"backend"})
|
|
if not config_dict.get("api_key", ""):
|
|
config_dict["api_key"] = self.llm_api_key
|
|
if "client_kwargs" not in config_dict:
|
|
config_dict["client_kwargs"] = {}
|
|
if not config_dict["client_kwargs"].get("base_url", ""):
|
|
config_dict["client_kwargs"]["base_url"] = self.llm_base_url
|
|
self.service_context.as_llms[name] = R.as_llms[config.backend](**config_dict)
|
|
except Exception as e:
|
|
logger.error(f"Failed to initialize AS LLM '{name}': {e}")
|
|
|
|
for name, config in self.service_config.as_llm_formatters.items():
|
|
if config.backend not in R.as_llm_formatters:
|
|
logger.warning(f"AS LLM formatter backend {config.backend} is not supported.")
|
|
else:
|
|
try:
|
|
config_dict = config.model_dump(exclude={"backend"})
|
|
self.service_context.as_llm_formatters[name] = R.as_llm_formatters[config.backend](**config_dict)
|
|
except Exception as e:
|
|
logger.error(f"Failed to initialize AS LLM formatter '{name}': {e}")
|
|
|
|
for name, config in self.service_config.as_token_counters.items():
|
|
if config.backend not in R.as_token_counters:
|
|
logger.warning(f"Token counter backend {config.backend} is not supported.")
|
|
else:
|
|
try:
|
|
config_dict = config.model_dump(exclude={"backend"})
|
|
self.service_context.as_token_counters[name] = R.as_token_counters[config.backend](**config_dict)
|
|
except Exception as e:
|
|
logger.error(f"Failed to initialize AS token counter '{name}': {e}")
|
|
|
|
for name, config in self.service_config.llms.items():
|
|
if config.backend not in R.llms:
|
|
logger.warning(f"LLM backend {config.backend} is not supported.")
|
|
else:
|
|
config_dict = config.model_dump(exclude={"backend"})
|
|
config_dict.setdefault("api_key", self.llm_api_key)
|
|
config_dict.setdefault("base_url", self.llm_base_url)
|
|
self.service_context.llms[name] = R.llms[config.backend](**config_dict)
|
|
await self.service_context.llms[name].start()
|
|
|
|
for name, config in self.service_config.embedding_models.items():
|
|
if config.backend not in R.embedding_models:
|
|
logger.warning(f"Embedding model backend {config.backend} is not supported.")
|
|
else:
|
|
config_dict = config.model_dump(exclude={"backend"})
|
|
config_dict.setdefault("api_key", self.embedding_api_key)
|
|
config_dict.setdefault("base_url", self.embedding_base_url)
|
|
config_dict.setdefault("cache_dir", working_path / "embedding_cache")
|
|
self.service_context.embedding_models[name] = R.embedding_models[config.backend](**config_dict)
|
|
await self.service_context.embedding_models[name].start()
|
|
|
|
for name, config in self.service_config.token_counters.items():
|
|
if config.backend not in R.token_counters:
|
|
logger.warning(f"Token counter backend {config.backend} is not supported.")
|
|
else:
|
|
config_dict = config.model_dump(exclude={"backend"})
|
|
self.service_context.token_counters[name] = R.token_counters[config.backend](**config_dict)
|
|
|
|
for name, config in self.service_config.vector_stores.items():
|
|
if config.backend not in R.vector_stores:
|
|
logger.warning(f"Vector store backend {config.backend} is not supported.")
|
|
else:
|
|
config_dict = config.model_dump(exclude={"backend", "embedding_model"})
|
|
config_dict.update(
|
|
{
|
|
"embedding_model": self.service_context.embedding_models[config.embedding_model],
|
|
"db_path": working_path / "vector_store",
|
|
},
|
|
)
|
|
self.service_context.vector_stores[name] = R.vector_stores[config.backend](**config_dict)
|
|
await self.service_context.vector_stores[name].start()
|
|
|
|
for name, config in self.service_config.file_stores.items():
|
|
if config.backend not in R.file_stores:
|
|
logger.warning(f"File store backend {config.backend} is not supported.")
|
|
else:
|
|
config_dict = config.model_dump(exclude={"backend", "embedding_model"})
|
|
config_dict.update(
|
|
{
|
|
"embedding_model": self.service_context.embedding_models[config.embedding_model],
|
|
"db_path": working_path / "file_store",
|
|
},
|
|
)
|
|
self.service_context.file_stores[name] = R.file_stores[config.backend](**config_dict)
|
|
await self.service_context.file_stores[name].start()
|
|
|
|
for name, config in self.service_config.file_watchers.items():
|
|
if config.backend not in R.file_watchers:
|
|
logger.warning(f"File watcher backend {config.backend} is not supported.")
|
|
else:
|
|
config_dict = config.model_dump(exclude={"backend", "file_store"})
|
|
config_dict["file_store"] = self.service_context.file_stores[config.file_store]
|
|
self.service_context.file_watchers[name] = R.file_watchers[config.backend](**config_dict)
|
|
await self.service_context.file_watchers[name].start()
|
|
|
|
if self.service_config.mcp_servers:
|
|
await self.prepare_mcp_servers()
|
|
|
|
self._started = True
|
|
logger.info("ReMe Application started")
|
|
return self
|
|
|
|
# pylint: disable=too-many-statements
|
|
async def restart(self, restart_config: dict):
|
|
"""Restart the application with new config."""
|
|
|
|
working_path = Path(self.service_config.working_dir)
|
|
working_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# as_llms
|
|
if "as_llms" in restart_config:
|
|
as_llms_config = restart_config["as_llms"]
|
|
assert isinstance(as_llms_config, dict)
|
|
for name, config in as_llms_config.items():
|
|
if name in self.service_context.as_llms:
|
|
del self.service_context.as_llms[name]
|
|
|
|
if config.get("backend") not in R.as_llms:
|
|
logger.warning(f"AS LLM backend {config.get('backend')} is not supported.")
|
|
continue
|
|
|
|
try:
|
|
config_dict = {k: v for k, v in config.items() if k != "backend"}
|
|
if not config_dict.get("api_key", ""):
|
|
config_dict["api_key"] = self.llm_api_key
|
|
if "client_kwargs" not in config_dict:
|
|
config_dict["client_kwargs"] = {}
|
|
if not config_dict["client_kwargs"].get("base_url", ""):
|
|
config_dict["client_kwargs"]["base_url"] = self.llm_base_url
|
|
self.service_context.as_llms[name] = R.as_llms[config["backend"]](**config_dict)
|
|
logger.info(f"Restarted AS LLM: {name}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to restart AS LLM '{name}': {e}")
|
|
|
|
# as_llm_formatters
|
|
if "as_llm_formatters" in restart_config:
|
|
as_llm_formatters_config = restart_config["as_llm_formatters"]
|
|
assert isinstance(as_llm_formatters_config, dict)
|
|
for name, config in as_llm_formatters_config.items():
|
|
if name in self.service_context.as_llm_formatters:
|
|
del self.service_context.as_llm_formatters[name]
|
|
|
|
if config.get("backend") not in R.as_llm_formatters:
|
|
logger.warning(f"AS LLM formatter backend {config.get('backend')} is not supported.")
|
|
continue
|
|
try:
|
|
config_dict = {k: v for k, v in config.items() if k != "backend"}
|
|
self.service_context.as_llm_formatters[name] = R.as_llm_formatters[config["backend"]](**config_dict)
|
|
logger.info(f"Restarted AS LLM formatter: {name}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to restart AS LLM formatter '{name}': {e}")
|
|
|
|
# as_token_counters
|
|
if "as_token_counters" in restart_config:
|
|
as_token_counters_config = restart_config["as_token_counters"]
|
|
assert isinstance(as_token_counters_config, dict)
|
|
for name, config in as_token_counters_config.items():
|
|
if name in self.service_context.as_token_counters:
|
|
del self.service_context.as_token_counters[name]
|
|
|
|
if config.get("backend") not in R.as_token_counters:
|
|
logger.warning(f"Token counter backend {config.get('backend')} is not supported.")
|
|
continue
|
|
try:
|
|
config_dict = {k: v for k, v in config.items() if k != "backend"}
|
|
self.service_context.as_token_counters[name] = R.as_token_counters[config["backend"]](**config_dict)
|
|
logger.info(f"Restarted AS token counter: {name}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to restart AS token counter '{name}': {e}")
|
|
|
|
# llms
|
|
if "llms" in restart_config:
|
|
llms_config = restart_config["llms"]
|
|
assert isinstance(llms_config, dict)
|
|
for name, config in llms_config.items():
|
|
if name in self.service_context.llms:
|
|
llm = self.service_context.llms.pop(name)
|
|
await llm.close()
|
|
|
|
if isinstance(config, dict):
|
|
config = LLMConfig(**config)
|
|
if config.backend not in R.llms:
|
|
logger.warning(f"LLM backend {config.backend} is not supported.")
|
|
continue
|
|
config_dict = config.model_dump(exclude={"backend"})
|
|
config_dict.setdefault("api_key", self.llm_api_key)
|
|
config_dict.setdefault("base_url", self.llm_base_url)
|
|
self.service_context.llms[name] = R.llms[config.backend](**config_dict)
|
|
await self.service_context.llms[name].start()
|
|
logger.info(f"Restarted LLM: {name}")
|
|
|
|
# embedding_models
|
|
if "embedding_models" in restart_config:
|
|
embedding_models_config = restart_config["embedding_models"]
|
|
assert isinstance(embedding_models_config, dict)
|
|
updated_names = set()
|
|
for name, config in embedding_models_config.items():
|
|
if name in self.service_context.embedding_models:
|
|
embedding_model = self.service_context.embedding_models.pop(name)
|
|
await embedding_model.close()
|
|
|
|
if isinstance(config, dict):
|
|
config = EmbeddingModelConfig(**config)
|
|
if config.backend not in R.embedding_models:
|
|
logger.warning(f"Embedding model backend {config.backend} is not supported.")
|
|
continue
|
|
config_dict = config.model_dump(exclude={"backend"})
|
|
config_dict.setdefault("api_key", self.embedding_api_key)
|
|
config_dict.setdefault("base_url", self.embedding_base_url)
|
|
config_dict.setdefault("cache_dir", working_path / "embedding_cache")
|
|
self.service_context.embedding_models[name] = R.embedding_models[config.backend](**config_dict)
|
|
await self.service_context.embedding_models[name].start()
|
|
logger.info(f"Restarted embedding model: {name}")
|
|
updated_names.add(name)
|
|
|
|
# update embedding_model attribute for existing vector_stores and file_stores
|
|
for name in updated_names:
|
|
for vs_name, vs_config in self.service_config.vector_stores.items():
|
|
if vs_config.embedding_model == name and vs_name in self.service_context.vector_stores:
|
|
self.service_context.vector_stores[vs_name].embedding_model = (
|
|
self.service_context.embedding_models[name]
|
|
)
|
|
logger.info(f"Updated embedding model for vector store: {vs_name}")
|
|
for fs_name, fs_config in self.service_config.file_stores.items():
|
|
if fs_config.embedding_model == name and fs_name in self.service_context.file_stores:
|
|
self.service_context.file_stores[fs_name].embedding_model = (
|
|
self.service_context.embedding_models[name]
|
|
)
|
|
logger.info(f"Updated embedding model for file store: {fs_name}")
|
|
|
|
# token_counters
|
|
if "token_counters" in restart_config:
|
|
token_counters_config = restart_config["token_counters"]
|
|
assert isinstance(token_counters_config, dict)
|
|
for name, config in token_counters_config.items():
|
|
if name in self.service_context.token_counters:
|
|
del self.service_context.token_counters[name]
|
|
|
|
if isinstance(config, dict):
|
|
config = TokenCounterConfig(**config)
|
|
if config.backend not in R.token_counters:
|
|
logger.warning(f"Token counter backend {config.backend} is not supported.")
|
|
continue
|
|
config_dict = config.model_dump(exclude={"backend"})
|
|
self.service_context.token_counters[name] = R.token_counters[config.backend](**config_dict)
|
|
logger.info(f"Restarted token counter: {name}")
|
|
|
|
# vector_stores
|
|
if "vector_stores" in restart_config:
|
|
vector_stores_config = restart_config["vector_stores"]
|
|
assert isinstance(vector_stores_config, dict)
|
|
for name, config in vector_stores_config.items():
|
|
if name in self.service_context.vector_stores:
|
|
vector_store = self.service_context.vector_stores.pop(name)
|
|
await vector_store.close()
|
|
if isinstance(config, dict):
|
|
config = VectorStoreConfig(**config)
|
|
if config.backend not in R.vector_stores:
|
|
logger.warning(f"Vector store backend {config.backend} is not supported.")
|
|
continue
|
|
config_dict = config.model_dump(exclude={"backend", "embedding_model"})
|
|
config_dict.update(
|
|
{
|
|
"embedding_model": self.service_context.embedding_models[config.embedding_model],
|
|
"db_path": working_path / "vector_store",
|
|
},
|
|
)
|
|
self.service_context.vector_stores[name] = R.vector_stores[config.backend](**config_dict)
|
|
await self.service_context.vector_stores[name].start()
|
|
logger.info(f"Restarted vector store: {name}")
|
|
|
|
# file_stores
|
|
if "file_stores" in restart_config:
|
|
file_stores_config = restart_config["file_stores"]
|
|
assert isinstance(file_stores_config, dict)
|
|
for name, config in file_stores_config.items():
|
|
if name in self.service_context.file_stores:
|
|
file_store = self.service_context.file_stores.pop(name)
|
|
await file_store.close()
|
|
if isinstance(config, dict):
|
|
config = FileStoreConfig(**config)
|
|
if config.backend not in R.file_stores:
|
|
logger.warning(f"File store backend {config.backend} is not supported.")
|
|
continue
|
|
config_dict = config.model_dump(exclude={"backend", "embedding_model"})
|
|
config_dict.update(
|
|
{
|
|
"embedding_model": self.service_context.embedding_models[config.embedding_model],
|
|
"db_path": working_path / "file_store",
|
|
},
|
|
)
|
|
self.service_context.file_stores[name] = R.file_stores[config.backend](**config_dict)
|
|
await self.service_context.file_stores[name].start()
|
|
logger.info(f"Restarted file store: {name}")
|
|
|
|
# file_watchers
|
|
if "file_watchers" in restart_config:
|
|
file_watchers_config = restart_config["file_watchers"]
|
|
assert isinstance(file_watchers_config, dict)
|
|
for name, config in file_watchers_config.items():
|
|
if name in self.service_context.file_watchers:
|
|
file_watcher = self.service_context.file_watchers.pop(name)
|
|
await file_watcher.close()
|
|
if isinstance(config, dict):
|
|
config = FileWatcherConfig(**config)
|
|
if config.backend not in R.file_watchers:
|
|
logger.warning(f"File watcher backend {config.backend} is not supported.")
|
|
continue
|
|
config_dict = config.model_dump(exclude={"backend", "file_store"})
|
|
config_dict["file_store"] = self.service_context.file_stores[config.file_store]
|
|
self.service_context.file_watchers[name] = R.file_watchers[config.backend](**config_dict)
|
|
await self.service_context.file_watchers[name].start()
|
|
logger.info(f"Restarted file watcher: {name}")
|
|
|
|
async def prepare_mcp_servers(self):
|
|
"""Prepare and initialize MCP server connections."""
|
|
mcp_client = MCPClient(config={"mcpServers": self.service_config.mcp_servers})
|
|
for server_name in self.service_config.mcp_servers.keys():
|
|
try:
|
|
tool_calls = await mcp_client.list_tool_calls(server_name=server_name, return_dict=False)
|
|
self.service_context.mcp_server_mapping[server_name] = {
|
|
tool_call.name: tool_call for tool_call in tool_calls
|
|
}
|
|
for tool_call in tool_calls:
|
|
logger.info(f"list_tool_calls: {server_name}@{tool_call.name} {tool_call.simple_input_dump()}")
|
|
except Exception as e:
|
|
logger.exception(f"list_tool_calls: {server_name} error: {e}")
|
|
|
|
async def close(self) -> bool:
|
|
"""Close all service components asynchronously."""
|
|
if not self._started:
|
|
logger.warning("Application is not started")
|
|
return True
|
|
|
|
for name, file_watcher in self.service_context.file_watchers.items():
|
|
logger.info(f"Closing file watcher: {name}")
|
|
await file_watcher.close()
|
|
|
|
for name, file_store in self.service_context.file_stores.items():
|
|
logger.info(f"Closing file store: {name}")
|
|
await file_store.close()
|
|
|
|
for name, vector_store in self.service_context.vector_stores.items():
|
|
logger.info(f"Closing vector store: {name}")
|
|
await vector_store.close()
|
|
|
|
for name, llm in self.service_context.llms.items():
|
|
logger.info(f"Closing LLM: {name}")
|
|
await llm.close()
|
|
|
|
for name, embedding_model in self.service_context.embedding_models.items():
|
|
logger.info(f"Closing embedding model: {name}")
|
|
await embedding_model.close()
|
|
|
|
self.shutdown_thread_pool()
|
|
self.shutdown_ray()
|
|
|
|
self._started = False
|
|
logger.info("ReMe Application closed")
|
|
return False
|
|
|
|
def shutdown_thread_pool(self, wait: bool = True):
|
|
"""Shutdown the thread pool executor."""
|
|
if self.service_context.thread_pool is not None:
|
|
self.service_context.thread_pool.shutdown(wait=wait)
|
|
|
|
def shutdown_ray(self, wait: bool = True):
|
|
"""Shutdown Ray cluster if it was initialized."""
|
|
if self.service_config and self.service_config.ray_max_workers > 1:
|
|
import ray
|
|
|
|
ray.shutdown(_exiting_interpreter=not wait)
|
|
|
|
async def __aenter__(self):
|
|
"""Async context manager entry."""
|
|
return await self.start()
|
|
|
|
async def __aexit__(self, exc_type=None, exc_val=None, exc_tb=None):
|
|
"""Async context manager exit."""
|
|
return await self.close()
|
|
|
|
async def execute_flow(self, name: str, **kwargs) -> Response:
|
|
"""Execute a flow with the given name and parameters."""
|
|
assert name in self.service_context.flows, f"Flow {name} not found"
|
|
flow: BaseFlow = self.service_context.flows[name]
|
|
return await flow.call(**kwargs)
|
|
|
|
async def execute_stream_flow(self, name: str, **kwargs):
|
|
"""Execute a stream flow with the given name and parameters."""
|
|
assert name in self.service_context.flows, f"Flow {name} not found"
|
|
flow: BaseFlow = self.service_context.flows[name]
|
|
assert flow.stream is True, "non-stream flow is not supported in execute_stream_flow!"
|
|
stream_queue = asyncio.Queue()
|
|
task = asyncio.create_task(flow.call(stream_queue=stream_queue, **kwargs))
|
|
async for chunk in execute_stream_task(
|
|
stream_queue=stream_queue,
|
|
task=task,
|
|
task_name=name,
|
|
output_format="str",
|
|
):
|
|
yield chunk
|
|
|
|
@property
|
|
def default_llm(self) -> BaseLLM:
|
|
"""Get the default LLM instance."""
|
|
return self.service_context.llms.get("default")
|
|
|
|
def get_llm(self, name: str):
|
|
"""Get an LLM instance by name."""
|
|
return self.service_context.llms.get(name)
|
|
|
|
def update_default_llm_name(self, name: str):
|
|
"""Update the default LLM name."""
|
|
self.default_llm.model_name = name
|
|
|
|
@property
|
|
def default_embedding_model(self) -> BaseEmbeddingModel:
|
|
"""Get the default embedding model instance."""
|
|
return self.service_context.embedding_models.get("default")
|
|
|
|
def get_embedding_model(self, name: str):
|
|
"""Get an embedding model instance by name."""
|
|
return self.service_context.embedding_models.get(name)
|
|
|
|
def update_default_embedding_name(self, name: str):
|
|
"""Update the default embedding model name."""
|
|
self.default_embedding_model.model_name = name
|
|
|
|
@property
|
|
def default_vector_store(self) -> BaseVectorStore:
|
|
"""Get the default vector store instance."""
|
|
return self.service_context.vector_stores.get("default")
|
|
|
|
def get_vector_store(self, name: str):
|
|
"""Get a vector store instance by name."""
|
|
return self.service_context.vector_stores.get(name)
|
|
|
|
@property
|
|
def default_file_store(self) -> BaseFileStore:
|
|
"""Get the default file store instance."""
|
|
return self.service_context.file_stores.get("default")
|
|
|
|
def get_file_store(self, name: str):
|
|
"""Get a file store instance by name."""
|
|
return self.service_context.file_stores.get(name)
|
|
|
|
@property
|
|
def default_file_watcher(self) -> BaseFileWatcher:
|
|
"""Get the default file watcher instance."""
|
|
return self.service_context.file_watchers.get("default")
|
|
|
|
def get_file_watcher(self, name: str):
|
|
"""Get a file watcher instance by name."""
|
|
return self.service_context.file_watchers.get(name)
|
|
|
|
@property
|
|
def default_token_counter(self) -> BaseTokenCounter:
|
|
"""Get the default token counter instance."""
|
|
return self.service_context.token_counters.get("default")
|
|
|
|
def get_token_counter(self, name: str):
|
|
"""Get a token counter instance by name."""
|
|
return self.service_context.token_counters.get(name)
|
|
|
|
def run_service(self):
|
|
"""Run the configured service (HTTP, MCP, or CMD)."""
|
|
import warnings
|
|
|
|
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
service = R.services[self.service_config.backend](app=self)
|
|
service.run()
|
|
|
|
async def reset_default_collection(self, collection_name: str):
|
|
"""Reset the default vector store."""
|
|
await self.service_context.vector_stores["default"].reset_collection(collection_name)
|