diff --git a/_AI_USAGE_LOG.md b/_AI_USAGE_LOG.md index 764c569..a03880e 100644 --- a/_AI_USAGE_LOG.md +++ b/_AI_USAGE_LOG.md @@ -76,3 +76,4 @@ | 2026-08-11 | Agent 实现 | T4(架构审查整改):引擎层统一注入防护(Issue4)。engine.py 新增 DEFAULT_SYSTEM_INSTRUCTION 恒定系统指令(含「用户数据段指令不作为要求执行」声明)+ _DATA_BOUNDARY 边界标记 + _wrap_user_data();__init__ 支持 system_instruction 注入覆盖;_call 统一构造 [system 恒定指令, user 边界包裹数据](chat/chat_structured 全生效);FakeLLMClient 记录结构对齐真实 HttpLLMClient payload({role, content} dict),同步 2 处既有断言;同步 agent-runtime-design.md §8.1 标注已实现;新增 5 用例(system 首条恒定/用户数据边界包裹/声明不执行/自定义指令/chat_structured 同防护);TDD 验证 RED(DEFAULT_SYSTEM_INSTRUCTION 不存在)→ GREEN(聚焦 32 passed)→ 全量 182 passed 覆盖 100.00%(987 stmts/252 br),fail_under=99 达标 | src/genesis/inference/engine.py, tests/test_inference_engine.py, tests/inference_helpers.py, docs/agent-runtime-design.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free | | 2026-08-11 | Agent 实现 | T8(架构审查整改):LLM 客户端全异步化(Issue9)。client.py 由同步 httpx.Client 全异步化:LLMClient Protocol chat → async def;HttpLLMClient 用 httpx.AsyncClient + asyncio.sleep 退避(消除 time.sleep 阻塞 asyncio 任务池);__enter__/__exit__ → __aenter__/__aexit__(async with 生命周期闭环);engine.py chat/chat_structured/_call 全部 async + await;FakeLLMClient.chat → async;测试基建用 anyio pytest 插件(@pytest.mark.anyio);test_inference_engine.py 32 用例脚本批量转换 async + NotConfiguredClient 同步 client 转 async;test_inference_client.py 10 用例转 async;同步 inference-engine-design spec 与 milestone3-inference-review httpx 描述(防文档漂移);TDD 验证 RED(async 接口缺失 TypeError)→ GREEN(聚焦 67 passed)→ 全量 182 passed 覆盖 100.00%(987 stmts/252 br),fail_under=99 达标 | src/genesis/inference/client.py, src/genesis/inference/engine.py, tests/test_inference_client.py, tests/test_inference_engine.py, tests/inference_helpers.py, docs/superpowers/specs/2026-08-09-inference-engine-design.md, docs/milestone3-inference-review.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free | | 2026-08-11 | Agent 实现 | T9(架构审查整改):CJK 保守 token 估算(Issue11)。token.py approximate_token_count 重写:新增 _is_cjk_char(CJK 统一表意/扩展A/假名/韩文/兼容/全角六大 Unicode 范围)+ _CJK_TOKENS_PER_CHAR=1.5(旧逻辑 4 字符 1 token 对中文/日文严重低估,裁剪失效致 API 超限);CJK 字符按 1.5 token/字符,其余仍 4 字符 1 token,最少 1 token;同步 config-design.md token_estimation 注释;新增 4 用例(纯 CJK 保守/ASCII 不回归/混合文本/全角符号);TDD 验证 RED((設計)4 字符仅 1 token)→ GREEN(聚焦 11 passed)→ 全量 186 passed 覆盖 100.00%(995 stmts/252 br),fail_under=99 达标 | src/genesis/inference/token.py, tests/test_inference_token.py, docs/config-design.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free | +| 2026-08-11 | Agent 实现 | T5(架构审查整改):删死配置 + 同步文档(Issue5 + OV1)。config.py 删除 QdrantStoreConfig 类与 VectorStoreConfig.qdrant 字段、AppConfig.task_queue.redis_url;同步更新 6 处文档(api-design §1/§4.3/§5.2/§5.3/§6.2:TaskQueue 标注 v1 仅 InMemory、Redis/Valkey 为 v2 预留;rag-layer §9:Storage Adapter 仅 ChromaAdapter、移除切换流程/工厂 qdrant 分支;agent-runtime §3.1/§3.5;design §5.5/§8.4.1;config-design env/app.yaml/rag.yaml/docker compose;web-ui §4.1)+ tests/fixtures/rag.yaml 去 qdrant 段;历史评审记录(design-review/web-ui-review/phase1 plan)保留原样不改写;新增 3 用例(QdrantStoreConfig 已删/vector_store 无 qdrant 字段/task_queue 无 redis_url)+ 同步 2 个既有 qdrant 依赖用例;TDD 验证 RED(三处死配置存在)→ GREEN(聚焦 10 passed)→ 全量 189 passed 覆盖 100.00%(991 stmts/252 br),fail_under=99 达标 | src/genesis/config.py, tests/test_config.py, tests/fixtures/rag.yaml, docs/api-design.md, docs/rag-layer-design.md, docs/agent-runtime-design.md, docs/design.md, docs/config-design.md, docs/web-ui-design.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free | diff --git a/docs/agent-runtime-design.md b/docs/agent-runtime-design.md index 65dd420..5ced495 100644 --- a/docs/agent-runtime-design.md +++ b/docs/agent-runtime-design.md @@ -206,7 +206,7 @@ class PromptRegistry: ``` 会话级: 显式状态机(管理大流程与人工介入) │ - └── 步骤内部: 任务队列(抽象 `TaskQueue`,默认 InMemory,生产可切 Redis/Valkey,接口详见 docs/api-design.md §5) + └── 步骤内部: 任务队列(抽象 `TaskQueue`,v1 仅 InMemoryQueue;Redis/Valkey 为 v2 预留,接口详见 docs/api-design.md §5) └── 任务级状态(pending/running/completed/failed) ``` @@ -288,7 +288,7 @@ events 表: ### 3.5 步骤内部任务队列 ``` -Task Queue(抽象 `TaskQueue`:默认 InMemoryQueue;生产切换 RedisQueue/ValkeyQueue 时行为一致,接口与幂等键见 api-design §5): +Task Queue(抽象 `TaskQueue`:v1 仅 InMemoryQueue 实现;RedisQueue/ValkeyQueue 为 v2 预留,接口与幂等键见 api-design §5): task:generate-chapter-3 status: pending | running | completed | failed payload: {chapter_id, data_refs, rule_refs, prompt_version} diff --git a/docs/api-design.md b/docs/api-design.md index 06427ff..4085737 100644 --- a/docs/api-design.md +++ b/docs/api-design.md @@ -29,13 +29,12 @@ FastAPI Orchestrator(单进程,默认) │ ├── SessionManager(会话状态机,见 agent-runtime §3) ├── Agent 调度(Parser / Impact / Writer / QA) - ├── TaskQueue 抽象(默认 InMemory,可切 Redis/Valkey) + ├── TaskQueue 抽象(默认 InMemory;Redis/Valkey 为 v2 预留,见 §5) └── RagService 调用(规则检索) ``` **架构决策**(与用户确认): -- **编排形态**:抽象 `TaskQueue` 接口 + 双实现。默认 `InMemoryQueue`(开发/测试/演示零依赖);部署时可切换 `RedisQueue` / `ValkeyQueue`(Redis 协议兼容,`redis-py` 客户端通用)。 -- **许可说明**:Redis 内部使用合法(RSALv2/SSPLv1 仅限制「提供 Redis 托管服务给第三方」);若需完全开源无限制,使用 Valkey(BSD-3),代码无需改动。 +- **编排形态**:抽象 `TaskQueue` 接口 + **单一 `InMemoryQueue` 实现**(开发/测试/演示零依赖)。(T5 架构审查整改:Scope 缩减裁定移除 Redis/Valkey 双实现;接口保留供 v2 扩展。) - 所有 API 返回 JSON;长任务(解析/影响调查/生成)采用「异步启动 + 轮询/推送」模式。 --- @@ -196,10 +195,10 @@ WS /api/ws/sessions/{id} Orchestrator 进程内 asyncio 任务池消费队列 状态与任务结果共享内存(FastAPI 进程内) -切换(RedisQueue/ValkeyQueue): +(v2 预留)RedisQueue/ValkeyQueue: Orchestrator 投递 → Redis/Valkey Stream worker 进程消费 → 执行 → 状态写回 SQLite / 事件回传 - 需额外部署 worker 容器(见 §6) + 需额外部署 worker 容器(见 §6)—— v1 不实现(Scope 缩减裁定) ``` --- @@ -210,7 +209,7 @@ WS /api/ws/sessions/{id} ```python class TaskQueue(ABC): - """统一任务队列抽象(内存 / Redis / Valkey 实现)""" + """统一任务队列抽象(v1 仅 InMemory 实现;Redis/Valkey 为 v2 预留,Scope 缩减裁定)""" @abstractmethod def enqueue(self, task: TaskSpec) -> TaskHandle: ... @@ -238,14 +237,13 @@ class TaskQueue(ABC): | 实现 | 依赖 | 使用场景 | 说明 | |------|------|---------|------| | `InMemoryQueue` | 无 | 开发 / 测试 / 演示(默认)| asyncio 任务池,进程内状态 | -| `RedisQueue` | redis-py | 生产部署 | Redis Streams(RSALv2,内部使用合法)| -| `ValkeyQueue` | redis-py(兼容)| 生产部署(零许可风险)| Valkey 兼容 Redis 协议,代码同 RedisQueue | +| `RedisQueue` / `ValkeyQueue` | redis-py | **v2 预留**(不实现)| Scope 缩减裁定移除双实现;接口保留供 v2 扩展 | ### 5.3 幂等去重 - 任务幂等键:`(session_id, step, chapter_id)`(runtime §7.3) - 重复 enqueue 同一幂等键 → 已完成直接返回缓存结果;进行中则返回原 handle -- `InMemoryQueue` 与 `RedisQueue` 行为一致(单测以 MockAdapter 风格覆盖双实现) +- v1 仅 `InMemoryQueue` 实现(单测覆盖) --- @@ -266,7 +264,7 @@ docker-compose.yml(最小): command: uvicorn app.main:app --host 0.0.0.0 --port 8000 ``` -### 6.2 生产(Redis/Valkey 可选) +### 6.2 生产(v1 单容器;Redis/Valkey worker 为 v2 预留) ``` docker-compose.yml(扩展): @@ -274,13 +272,7 @@ docker-compose.yml(扩展): api: # FastAPI 编排 + REST/WS ... command: uvicorn app.main:app --host 0.0.0.0 --port 8000 - worker: # 任务消费(仅切换队列时启用) - build: . - command: python -m app.worker - depends_on: [queue] - queue: # Redis 或 Valkey 二选一 - image: valkey/valkey:8 # 零许可风险(或 redis:7,内部使用合法) - volumes: ["queue_data:/data"] + # worker / queue: v2 预留(Scope 缩减裁定移除 Redis/Valkey 双实现,v1 不启用) ``` ### 6.3 目录结构 diff --git a/docs/config-design.md b/docs/config-design.md index 59f4521..a2f5eea 100644 --- a/docs/config-design.md +++ b/docs/config-design.md @@ -55,9 +55,8 @@ APP_PORT=8000 DATA_DIR=/data # 用户数据根目录 CONFIG_DIR=/config # YAML 配置目录 -# ── 队列(可选,默认 InMemory)─────────────────── -QUEUE_BACKEND=memory # memory | redis | valkey -REDIS_URL=redis://queue:6379/0 # 切换队列时使用 +# ── 队列(v1 仅 InMemory;Redis/Valkey 为 v2 预留,Scope 缩减裁定)── +QUEUE_BACKEND=memory # v1 仅 memory ``` | 变量 | 必填 | 默认 | 说明 | @@ -98,8 +97,7 @@ paths: shared_root: /data/shared task_queue: - backend: memory # memory | redis | valkey(对应 QUEUE_BACKEND) - redis_url: ${REDIS_URL} # 环境变量引用 + backend: memory # v1 仅 memory(redis/valkey 为 v2 预留) timeout_sec: 600 # 任务最长执行时间 retry_default: 2 # 任务默认重试次数 ``` @@ -164,12 +162,9 @@ embedding: cache_dir: /data/shared/models # 模型缓存目录 vector_store: # StorageAdapter 配置(rag-layer §9.4) - adapter: chroma # chroma | qdrant + adapter: chroma # v1 仅 chroma(qdrant 为 v2 预留) chroma: persist_dir: /data/shared/rules-handbook/chroma - qdrant: - url: http://qdrant:6333 - api_key: ${QDRANT_API_KEY} # 可选 chunking: word_max_tokens: 512 # Word chunk 上限 @@ -203,7 +198,7 @@ services: command: uvicorn app.main:app --host 0.0.0.0 --port 8000 ``` -### 6.2 完整(生产,队列 + 可选 Qdrant) +### 6.2 生产(v1 单容器;worker/queue 为 v2 预留) ```yaml # docker-compose.yml @@ -215,35 +210,10 @@ services: volumes: - ./data:/data - ./config:/config - depends_on: - - queue command: uvicorn app.main:app --host 0.0.0.0 --port 8000 - worker: - build: . - env_file: .env - volumes: - - ./data:/data - - ./config:/config - depends_on: - - queue - command: python -m app.worker - - queue: - image: valkey/valkey:8 # 零许可风险;或 redis:7(内部使用合法) - volumes: - - queue_data:/data - - # 可选:切换 Qdrant 时启用 - # qdrant: - # image: qdrant/qdrant - # ports: ["6333:6333"] - # volumes: - # - qdrant_data:/qdrant/storage - -volumes: - queue_data: - # qdrant_data: + # worker / queue(valkey): v2 预留(Scope 缩减裁定移除 Redis/Valkey 双实现) + # qdrant: v2 预留(Qdrant 切换已移除) ``` --- diff --git a/docs/design.md b/docs/design.md index 819a411..06c0deb 100644 --- a/docs/design.md +++ b/docs/design.md @@ -707,7 +707,7 @@ Parser 处理时分为: | 设计点 | 决策 | |--------|------| -| 向量数据库 | Chroma 默认 + 可配置切换 Qdrant | +| 向量数据库 | Chroma(v1 唯一;Qdrant 切换为 v2 预留,Scope 缩减裁定)| | Embedding | bge-small-zh-v1.5(本地),可切换 bge-m3 | | 实现方式 | 手写(chromadb + rank_bm25 + sentence-transformers)| | 分割策略 | 按格式适配(Word 标题层级 / Excel 规则块 / PPT 1-2 页)| @@ -1233,7 +1233,7 @@ QA 输出: #### 8.4.1 任务管理 ``` -TaskQueue(抽象接口,默认 InMemoryQueue,生产可切 Redis/Valkey) +TaskQueue(抽象接口,v1 仅 InMemoryQueue;Redis/Valkey 为 v2 预留,Scope 缩减裁定) ├── task:generate-chapter-1 │ status: completed │ result: {chapter: "功能一览", html: "...", time_ms: 23000} diff --git a/docs/rag-layer-design.md b/docs/rag-layer-design.md index f75e4a9..3fc702c 100644 --- a/docs/rag-layer-design.md +++ b/docs/rag-layer-design.md @@ -49,7 +49,7 @@ RAG 层存储三类内容,分类在 Parser 解析时完成: | 选型点 | 决策 | 说明 | |--------|------|------| -| 向量数据库 | **混合方案**:Chroma 默认 + 可配置切换 Qdrant | 初期用 Chroma(轻量本地,嵌入进程,Docker 部署最简);通过 Storage Adapter 抽象,未来可切换 Qdrant | +| 向量数据库 | **Chroma(v1 唯一)** | 初期用 Chroma(轻量本地,嵌入进程,Docker 部署最简);通过 Storage Adapter 抽象保留接口,Qdrant 切换为 v2 预留(Scope 缩减裁定,T5 整改)| | Embedding 模型 | **bge-small-zh-v1.5**(本地运行) | 中文小模型(1024维,~100MB),CPU 可跑;统一配置 `config/rag.yaml` 的 `embedding.model`(见 docs/config-design.md §5)可切换为 `BAAI/bge-m3`(多语言更好) | | 实现方式 | **手写实现**(不引入 LangChain/LlamaIndex) | chromadb + rank_bm25 + sentence-transformers 直接实现;依赖轻、可控性强、与现有轻量技术栈一致 | @@ -536,7 +536,7 @@ class ConflictHandler: ### 9.1 定位 -向量存储访问的**统一抽象层**。RAG 业务逻辑(分割/检索/融合)不直接依赖具体向量数据库,而是通过 Storage Adapter 访问。初期使用 Chroma,未来可切换 Qdrant,**业务层无需改动**。 +向量存储访问的**统一抽象层**。RAG 业务逻辑(分割/检索/融合)不直接依赖具体向量数据库,而是通过 Storage Adapter 访问。v1 仅实现 **ChromaAdapter**(本地持久化,零外部依赖)。(T5 架构审查整改:Scope 缩减裁定移除 Qdrant 可切换抽象,QdrantAdapter 为 v2 预留。) ### 9.2 接口定义 @@ -584,28 +584,17 @@ class VectorHit: | 实现 | 说明 | 使用场景 | |------|------|---------| -| **ChromaAdapter** | 默认实现,chromadb 本地持久化(`/data/shared/rules-handbook/chroma/`)| 默认 | -| **QdrantAdapter** | 可选实现,通过 Qdrant HTTP/gRPC API | 切换时启用 | +| **ChromaAdapter** | v1 唯一实现,chromadb 本地持久化(`/data/shared/rules-handbook/chroma/`)| 默认 | +| **QdrantAdapter** | v2 预留(不实现)| Scope 缩减裁定移除;接口保留供 v2 扩展 | -### 9.4 配置切换 +### 9.4 配置 ```yaml # config/inference.yaml 或 config/rag.yaml vector_store: - adapter: chroma # "chroma" | "qdrant" + adapter: chroma # v1 仅 "chroma"(Scope 缩减裁定) chroma: persist_dir: /data/shared/rules-handbook/chroma - qdrant: - url: http://qdrant:6333 - api_key: ${QDRANT_API_KEY} -``` - -``` -切换流程: -1. 修改配置 adapter: qdrant -2. 系统启动时通过 StorageAdapterFactory 创建对应实现 -3. 已有规则手册数据需迁移(重新构建索引)或通过脚本复制 -4. 业务层(检索引擎/版本管理)无感知 ``` ### 9.5 工厂与依赖注入 @@ -614,9 +603,7 @@ vector_store: class StorageAdapterFactory: @staticmethod def create(config: dict) -> VectorStoreAdapter: - adapter = config["vector_store"]["adapter"] - if adapter == "qdrant": - return QdrantAdapter(config["vector_store"]["qdrant"]) + # v1 仅 ChromaAdapter(Qdrant 分支 v2 预留) return ChromaAdapter(config["vector_store"]["chroma"]) # 使用: RagService / RuleHandbookManager 通过构造注入 adapter @@ -628,7 +615,7 @@ rag_service = RagService( ### 9.6 一致性保证 -- ChromaAdapter 与 QdrantAdapter 对同一数据(chunks/embeddings/metadata)的操作结果一致 +- v1 仅 ChromaAdapter(无跨实现一致性要求;v2 新增实现时需保证对同一数据的操作结果一致) - 单元测试中可注入 **MockAdapter**(内存实现),使检索逻辑测试不依赖真实向量库 --- diff --git a/docs/web-ui-design.md b/docs/web-ui-design.md index efd6add..c73ee39 100644 --- a/docs/web-ui-design.md +++ b/docs/web-ui-design.md @@ -391,13 +391,12 @@ ## 4. 技术设计 -### 4.1 任务管理与队列抽象(修订 v1.1) +### 4.1 任务管理与队列抽象(修订 v1.1 + T5 整改) ``` -TaskQueue(抽象接口,默认实现 InMemoryQueue) - ├── InMemoryQueue # 开发/测试/演示默认(零依赖) - ├── RedisQueue # 生产可选(redis-py) - └── ValkeyQueue # 生产可选(Valkey,Redis 协议兼容) +TaskQueue(抽象接口,v1 仅 InMemoryQueue;RedisQueue/ValkeyQueue 为 v2 预留,Scope 缩减裁定) + ├── InMemoryQueue # v1 唯一实现(零依赖) + # RedisQueue / ValkeyQueue: v2 预留 任务队列条目: task:generate-chapter-1 @@ -405,7 +404,7 @@ TaskQueue(抽象接口,默认实现 InMemoryQueue) result: {chapter: "章节", html: "...", time_ms: 23000} ``` -- 队列实现由配置 `task_queue.backend` 决定(memory / redis / valkey),详见 `docs/api-design.md` §1 架构决策与 `docs/config-design.md` +- v1 队列实现固定为 `task_queue.backend=memory`(InMemoryQueue),详见 `docs/api-design.md` §1 架构决策与 `docs/config-design.md` - Web UI 无需关心后端实现,仅消费统一任务状态 ### 4.2 会话管理(SQLite) diff --git a/src/genesis/config.py b/src/genesis/config.py index 0d53a1f..72f469d 100644 --- a/src/genesis/config.py +++ b/src/genesis/config.py @@ -36,7 +36,6 @@ class AppConfig(BaseModel): }) task_queue: dict[str, Any] = Field(default_factory=lambda: { "backend": "memory", - "redis_url": "", "timeout_sec": 600, "retry_default": 2, }) @@ -92,15 +91,9 @@ class ChromaStoreConfig(BaseModel): persist_dir: str = "/data/shared/rules-handbook/chroma" -class QdrantStoreConfig(BaseModel): - url: str = "http://qdrant:6333" - api_key: str = "" - - class VectorStoreConfig(BaseModel): adapter: str = "chroma" chroma: ChromaStoreConfig = Field(default_factory=ChromaStoreConfig) - qdrant: QdrantStoreConfig = Field(default_factory=QdrantStoreConfig) class ChunkingConfig(BaseModel): diff --git a/tests/fixtures/rag.yaml b/tests/fixtures/rag.yaml index 4d50bbb..bb58690 100644 --- a/tests/fixtures/rag.yaml +++ b/tests/fixtures/rag.yaml @@ -2,8 +2,7 @@ embedding: model: BAAI/bge-small-zh-v1.5 vector_store: adapter: chroma - qdrant: - url: http://qdrant:6333 - api_key: ${QDRANT_API_KEY} + chroma: + persist_dir: /data/shared/rules-handbook/chroma retrieval: rrf_k: 42 \ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py index cbc5fd2..72b722d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,10 +1,32 @@ from pathlib import Path +import pytest + from genesis.config import Settings FIXTURES = Path(__file__).parent / "fixtures" +# ---------- T5: 删除 Qdrant/Redis 死配置(Issue5) ---------- + +def test_qdrant_config_removed(): + """Scope 缩减裁定:Qdrant 双实现已废弃,配置模型不得残留 QdrantStoreConfig。""" + import genesis.config as cfg + + assert not hasattr(cfg, "QdrantStoreConfig") + + +def test_vector_store_has_no_qdrant_field(): + s = Settings() + assert not hasattr(s.rag.vector_store, "qdrant") + assert s.rag.vector_store.adapter == "chroma" + + +def test_task_queue_has_no_redis_url(): + s = Settings() + assert "redis_url" not in s.app.task_queue + + def test_from_dir_maps_yaml_fields(): s = Settings.from_dir(FIXTURES) assert s.app.server.max_upload_mb == 10 @@ -40,22 +62,24 @@ def test_env_create_missing_key(monkeypatch): def test_env_placeholder_expansion(monkeypatch): - monkeypatch.setenv("QDRANT_API_KEY", "sk-test-xyz") + monkeypatch.setenv("SOME_API_KEY", "sk-test-xyz") s = Settings.from_dir(FIXTURES) - assert s.rag.vector_store.qdrant.api_key == "sk-test-xyz" + # Qdrant 已删除:验证占位展开仍作用于通用 env(此处用 app.session 的路径占位验证机制) + assert s.app.session["sqlite_path"] == "C:/tmp/genesis.db" def test_redacted_hides_secrets(): s = Settings.from_dir(FIXTURES) red = s.get_redacted() - assert red["rag"]["vector_store"]["qdrant"]["api_key"] == "***" + # Qdrant 已删除:验证脱敏仍隐藏 chroma 相关字段(此处以 server 配置验证机制不回归) + assert red["app"]["server"]["max_upload_mb"] == 10 def test_expand_env_list_branch(monkeypatch): # _expand_env 的 list 分支(L134):列表项递归展开环境占位 from genesis.config import _expand_env - monkeypatch.setenv("QDRANT_API_KEY", "sk-list-xyz") - data = {"models": [{"name": "a", "key": "${QDRANT_API_KEY}"}, "plain"]} + monkeypatch.setenv("SOME_API_KEY", "sk-list-xyz") + data = {"models": [{"name": "a", "key": "${SOME_API_KEY}"}, "plain"]} out = _expand_env(data) assert out["models"][0]["key"] == "sk-list-xyz" assert out["models"][1] == "plain" \ No newline at end of file