From 68fd8660ada15943f11f41f3cfa894fcc3e15e9e Mon Sep 17 00:00:00 2001 From: aplicity <1634594707@qq.com> Date: Sat, 4 Apr 2026 22:16:16 +0800 Subject: [PATCH 1/4] chore: convert demo2-starter into staged starter --- README.md | 9 +++ demo2_task_agent/priority_queue.py | 28 +++---- demo2_task_agent/task_store.py | 51 +++++-------- demo2_task_agent/tool_registry.py | 52 ++++--------- demo3_ml_visual/data_loader.py | 26 ------- demo3_ml_visual/feature_engineer.py | 24 ------ demo3_ml_visual/main.py | 20 ----- demo3_ml_visual/model_io.py | 11 --- demo3_ml_visual/model_trainer.py | 42 ----------- .../main.py" | 75 ------------------- demo5_full_project/app/__init__.py | 1 - demo5_full_project/app/core/__init__.py | 1 - demo5_full_project/app/core/rag/__init__.py | 1 - demo5_full_project/app/core/rag/embedder.py | 21 ------ demo5_full_project/app/core/rag/retriever.py | 35 --------- demo5_full_project/app/ml/__init__.py | 1 - demo5_full_project/app/ml/model_io.py | 24 ------ demo5_full_project/app/ml/tuner.py | 26 ------- demo5_full_project/main.py | 75 ------------------- 19 files changed, 53 insertions(+), 470 deletions(-) delete mode 100644 demo3_ml_visual/data_loader.py delete mode 100644 demo3_ml_visual/feature_engineer.py delete mode 100644 demo3_ml_visual/main.py delete mode 100644 demo3_ml_visual/model_io.py delete mode 100644 demo3_ml_visual/model_trainer.py delete mode 100644 "demo4_\347\273\274\345\220\210\351\241\271\347\233\256/main.py" delete mode 100644 demo5_full_project/app/__init__.py delete mode 100644 demo5_full_project/app/core/__init__.py delete mode 100644 demo5_full_project/app/core/rag/__init__.py delete mode 100644 demo5_full_project/app/core/rag/embedder.py delete mode 100644 demo5_full_project/app/core/rag/retriever.py delete mode 100644 demo5_full_project/app/ml/__init__.py delete mode 100644 demo5_full_project/app/ml/model_io.py delete mode 100644 demo5_full_project/app/ml/tuner.py delete mode 100644 demo5_full_project/main.py diff --git a/README.md b/README.md index 77b381d..1f8e03b 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,15 @@ Agent · ML · FastAPI · RAG · SSE · CI Unlock Flow

+## 当前分支 + +你现在位于 `demo2-starter`。 + +- Demo1 可以作为已完成参考继续复用 +- 当前需要自己完成 `demo2_task_agent/` 的工具注册、任务存储和优先级队列 +- 通过本关后,push 到 `demo2-starter` 会自动解锁 Demo3 +- 完整答案仍然保留在 `main` 分支 + ## 快速导航 - [在线演示](#在线演示) diff --git a/demo2_task_agent/priority_queue.py b/demo2_task_agent/priority_queue.py index 0a0e411..070da54 100644 --- a/demo2_task_agent/priority_queue.py +++ b/demo2_task_agent/priority_queue.py @@ -1,23 +1,17 @@ -from __future__ import annotations - -import heapq -from typing import List, Tuple - - class PriorityQueue: - _priority_rank = {"high": 0, "mid": 1, "medium": 1, "low": 2} + """Demo2 starter: use heapq or another structure to sort tasks.""" def __init__(self) -> None: - self._heap: List[Tuple[int, int, dict]] = [] - self._counter = 0 + self._items = [] - def push(self, task: dict) -> None: - rank = self._priority_rank.get(task.get("priority", "low"), 2) - heapq.heappush(self._heap, (rank, self._counter, task)) - self._counter += 1 + def push(self, task): + """TODO: insert task according to priority.""" + raise NotImplementedError("Implement PriorityQueue.push for Demo2") - def pop(self) -> dict: - return heapq.heappop(self._heap)[2] + def pop(self): + """TODO: pop the highest-priority task.""" + raise NotImplementedError("Implement PriorityQueue.pop for Demo2") - def peek(self) -> dict: - return self._heap[0][2] + def peek(self): + """TODO: read the current highest-priority task without removing it.""" + raise NotImplementedError("Implement PriorityQueue.peek for Demo2") diff --git a/demo2_task_agent/task_store.py b/demo2_task_agent/task_store.py index 4cc42d6..3047409 100644 --- a/demo2_task_agent/task_store.py +++ b/demo2_task_agent/task_store.py @@ -1,40 +1,25 @@ -from __future__ import annotations - -from dataclasses import dataclass, asdict -from typing import Dict, List, Optional -from uuid import uuid4 - - -@dataclass -class Task: - id: str - title: str - priority: str - due_date: str - - class TaskStore: + """Demo2 starter: implement an in-memory task store.""" + def __init__(self) -> None: - self._tasks: Dict[str, Task] = {} + self._tasks = {} - def add(self, title: str, priority: str, due_date: str) -> dict: - task = Task(id=str(uuid4()), title=title, priority=priority, due_date=due_date) - self._tasks[task.id] = task - return asdict(task) + def add(self, title, priority, due_date): + """TODO: create a task dict with a unique id and store it.""" + raise NotImplementedError("Implement TaskStore.add for Demo2") - def get_all(self) -> List[dict]: - return [asdict(task) for task in self._tasks.values()] + def get_all(self): + """TODO: return every task as a list.""" + raise NotImplementedError("Implement TaskStore.get_all for Demo2") - def get(self, task_id: str) -> Optional[dict]: - task = self._tasks.get(task_id) - return asdict(task) if task else None + def get(self, task_id): + """TODO: return one task or None.""" + raise NotImplementedError("Implement TaskStore.get for Demo2") - def update(self, task_id: str, **kwargs) -> dict: - task = self._tasks[task_id] - for field in ("title", "priority", "due_date"): - if field in kwargs and kwargs[field] is not None: - setattr(task, field, kwargs[field]) - return asdict(task) + def update(self, task_id, **kwargs): + """TODO: update title / priority / due_date.""" + raise NotImplementedError("Implement TaskStore.update for Demo2") - def delete(self, task_id: str) -> bool: - return self._tasks.pop(task_id, None) is not None + def delete(self, task_id): + """TODO: remove the task and return whether it existed.""" + raise NotImplementedError("Implement TaskStore.delete for Demo2") diff --git a/demo2_task_agent/tool_registry.py b/demo2_task_agent/tool_registry.py index 41f6da7..625c44d 100644 --- a/demo2_task_agent/tool_registry.py +++ b/demo2_task_agent/tool_registry.py @@ -1,40 +1,18 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any, Callable, Dict - - -class ToolRegistryError(Exception): - pass - - -class PermissionDeniedError(ToolRegistryError): - pass - - -class ToolNotFoundError(ToolRegistryError): - pass - - -@dataclass -class ToolSpec: - func: Callable[..., Any] - permission: str - - class ToolRegistry: - _permission_order = {"read": 1, "write": 2, "admin": 3} + """Demo2 starter: build your own registry with permission checks.""" def __init__(self) -> None: - self._tools: Dict[str, ToolSpec] = {} - - def register(self, name: str, func: Callable[..., Any], permission: str) -> None: - self._tools[name] = ToolSpec(func=func, permission=permission) - - def call(self, name: str, args: Dict[str, Any], ctx_permission: str) -> Any: - if name not in self._tools: - raise ToolNotFoundError(name) - spec = self._tools[name] - if self._permission_order.get(ctx_permission, 0) < self._permission_order.get(spec.permission, 0): - raise PermissionDeniedError(f"{ctx_permission} cannot call {name}") - return spec.func(**args) + self._tools = {} + + def register(self, name, func, permission): + """TODO: save tool metadata into the registry.""" + raise NotImplementedError("Implement ToolRegistry.register for Demo2") + + def call(self, name, args, ctx_permission): + """ + TODO: + 1. check whether the tool exists + 2. validate permission level + 3. execute the tool with args + """ + raise NotImplementedError("Implement ToolRegistry.call for Demo2") diff --git a/demo3_ml_visual/data_loader.py b/demo3_ml_visual/data_loader.py deleted file mode 100644 index 4fb71a0..0000000 --- a/demo3_ml_visual/data_loader.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -from typing import Tuple - -import pandas as pd -from sklearn.datasets import load_iris, load_wine, load_breast_cancer - - -class DataLoader: - def load_csv(self, path: str) -> Tuple[pd.DataFrame, pd.Series]: - df = pd.read_csv(path) - if df.shape[1] < 2: - raise ValueError("CSV must contain features and a target column") - return df.iloc[:, :-1], df.iloc[:, -1] - - def load_builtin(self, name: str = "iris"): - mapping = { - "iris": load_iris, - "wine": load_wine, - "breast_cancer": load_breast_cancer, - } - if name not in mapping: - raise ValueError(f"Unsupported builtin dataset: {name}") - data = mapping[name](return_X_y=True) - return data diff --git a/demo3_ml_visual/feature_engineer.py b/demo3_ml_visual/feature_engineer.py deleted file mode 100644 index 1df1846..0000000 --- a/demo3_ml_visual/feature_engineer.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -import numpy as np -from sklearn.impute import SimpleImputer -from sklearn.pipeline import Pipeline -from sklearn.preprocessing import StandardScaler - - -class FeatureEngineer: - def __init__(self) -> None: - self.pipeline = Pipeline( - steps=[ - ("imputer", SimpleImputer(strategy="median")), - ("scaler", StandardScaler()), - ] - ) - - def fit_transform(self, X): - transformed = self.pipeline.fit_transform(X) - return np.asarray(transformed) - - def transform(self, X): - transformed = self.pipeline.transform(X) - return np.asarray(transformed) diff --git a/demo3_ml_visual/main.py b/demo3_ml_visual/main.py deleted file mode 100644 index eaa504f..0000000 --- a/demo3_ml_visual/main.py +++ /dev/null @@ -1,20 +0,0 @@ -from sklearn.model_selection import train_test_split - -from data_loader import DataLoader -from feature_engineer import FeatureEngineer -from model_trainer import ModelTrainer - - -def main() -> None: - X, y = DataLoader().load_builtin("iris") - fe = FeatureEngineer() - X_t = fe.fit_transform(X) - X_train, X_test, y_train, y_test = train_test_split(X_t, y, test_size=0.2, random_state=42) - trainer = ModelTrainer() - models = trainer.train(X_train, y_train) - metrics = trainer.evaluate(models, X_test, y_test) - print(metrics) - - -if __name__ == "__main__": - main() diff --git a/demo3_ml_visual/model_io.py b/demo3_ml_visual/model_io.py deleted file mode 100644 index f59ec06..0000000 --- a/demo3_ml_visual/model_io.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import annotations - -import joblib - - -def save_model(model, path: str) -> None: - joblib.dump(model, path) - - -def load_model(path: str): - return joblib.load(path) diff --git a/demo3_ml_visual/model_trainer.py b/demo3_ml_visual/model_trainer.py deleted file mode 100644 index 8ec1b8d..0000000 --- a/demo3_ml_visual/model_trainer.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations - -from typing import Dict - -from sklearn.ensemble import RandomForestClassifier -from sklearn.linear_model import LogisticRegression -from sklearn.metrics import accuracy_score, f1_score - -try: - from xgboost import XGBClassifier # type: ignore -except Exception: # pragma: no cover - XGBClassifier = None - - -class ModelTrainer: - def train(self, X_train, y_train) -> Dict[str, object]: - models: Dict[str, object] = { - "logistic_regression": LogisticRegression(max_iter=500), - "random_forest": RandomForestClassifier(n_estimators=100, random_state=42), - } - if XGBClassifier is not None: - models["xgboost"] = XGBClassifier( - n_estimators=50, - max_depth=3, - learning_rate=0.1, - eval_metric="mlogloss", - random_state=42, - ) - - for model in models.values(): - model.fit(X_train, y_train) - return models - - def evaluate(self, models, X_test, y_test): - metrics = {} - for name, model in models.items(): - preds = model.predict(X_test) - metrics[name] = { - "accuracy": float(accuracy_score(y_test, preds)), - "f1": float(f1_score(y_test, preds, average="weighted")), - } - return metrics diff --git "a/demo4_\347\273\274\345\220\210\351\241\271\347\233\256/main.py" "b/demo4_\347\273\274\345\220\210\351\241\271\347\233\256/main.py" deleted file mode 100644 index 8612b7a..0000000 --- "a/demo4_\347\273\274\345\220\210\351\241\271\347\233\256/main.py" +++ /dev/null @@ -1,75 +0,0 @@ -from __future__ import annotations - -import json -from typing import Iterator, List -from uuid import uuid4 - -from fastapi import FastAPI, HTTPException -from pydantic import BaseModel -from fastapi.responses import StreamingResponse - - -app = FastAPI(title="Demo4 Integrated App") -TASKS = {} - - -class ChatRequest(BaseModel): - message: str - - -class TaskCreateRequest(BaseModel): - title: str - priority: str - due_date: str - - -@app.get("/health") -def health(): - return {"status": "ok", "version": "0.4.0"} - - -@app.post("/chat") -def chat(payload: ChatRequest): - message = payload.message.strip() - tools_used: List[str] = [] - if "任务" in message and any(token in message for token in ["列", "list", "所有"]): - tools_used.append("list_tasks") - reply = f"当前共有 {len(TASKS)} 个任务。" - elif any(token in message for token in ["你好", "hi", "hello"]): - reply = "你好,这里是 Demo4 综合项目接口。" - else: - reply = "消息已收到,我可以处理聊天和任务相关请求。" - return {"reply": reply, "tools_used": tools_used} - - -@app.get("/tasks") -def get_tasks(): - return {"tasks": list(TASKS.values())} - - -@app.post("/tasks", status_code=201) -def create_task(payload: TaskCreateRequest): - task = payload.model_dump() - task["id"] = str(uuid4()) - TASKS[task["id"]] = task - return task - - -@app.delete("/tasks/{task_id}") -def delete_task(task_id: str): - if task_id not in TASKS: - raise HTTPException(status_code=404, detail="Task not found") - del TASKS[task_id] - return {"deleted": True} - - -def _sse_event_stream(message: str) -> Iterator[str]: - for chunk in ["demo4", "stream", message]: - payload = json.dumps({"type": "token", "content": chunk}, ensure_ascii=False) - yield f"data: {payload}\n\n" - yield "data: {\"type\": \"done\"}\n\n" - - -@app.get("/stream") -def stream(message: str): - return StreamingResponse(_sse_event_stream(message), media_type="text/event-stream") diff --git a/demo5_full_project/app/__init__.py b/demo5_full_project/app/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/demo5_full_project/app/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/demo5_full_project/app/core/__init__.py b/demo5_full_project/app/core/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/demo5_full_project/app/core/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/demo5_full_project/app/core/rag/__init__.py b/demo5_full_project/app/core/rag/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/demo5_full_project/app/core/rag/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/demo5_full_project/app/core/rag/embedder.py b/demo5_full_project/app/core/rag/embedder.py deleted file mode 100644 index b4ee5dd..0000000 --- a/demo5_full_project/app/core/rag/embedder.py +++ /dev/null @@ -1,21 +0,0 @@ -from __future__ import annotations - -import hashlib - -import numpy as np - - -class Embedder: - def __init__(self, dimension: int = 32): - self.dimension = dimension - - def encode(self, text: str) -> np.ndarray: - text = text or "" - buckets = np.zeros(self.dimension, dtype=float) - for token in text.encode("utf-8"): - buckets[token % self.dimension] += 1.0 - digest = hashlib.sha256(text.encode("utf-8")).digest() - for idx, byte in enumerate(digest[: self.dimension]): - buckets[idx] += byte / 255.0 - norm = np.linalg.norm(buckets) - return buckets if norm == 0 else buckets / norm diff --git a/demo5_full_project/app/core/rag/retriever.py b/demo5_full_project/app/core/rag/retriever.py deleted file mode 100644 index 66146da..0000000 --- a/demo5_full_project/app/core/rag/retriever.py +++ /dev/null @@ -1,35 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import List - -import numpy as np - -from app.core.rag.embedder import Embedder - - -@dataclass -class DocumentChunk: - text: str - - -class Retriever: - def __init__(self) -> None: - self.embedder = Embedder() - self._documents = [ - DocumentChunk("Agent 是一种能够感知、决策并执行动作的软件实体。"), - DocumentChunk("RAG 会先检索知识,再把检索结果拼到生成提示词中。"), - DocumentChunk("SSE 适合服务端向客户端单向推送流式文本。"), - ] - - def retrieve(self, query: str, top_k: int = 3) -> List[str]: - if not self._documents: - return [] - query_vec = self.embedder.encode(query) - scored = [] - for doc in self._documents: - doc_vec = self.embedder.encode(doc.text) - score = float(np.dot(query_vec, doc_vec)) - scored.append((score, doc.text)) - scored.sort(reverse=True, key=lambda item: item[0]) - return [text for _, text in scored[: max(0, min(top_k, 5))]] diff --git a/demo5_full_project/app/ml/__init__.py b/demo5_full_project/app/ml/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/demo5_full_project/app/ml/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/demo5_full_project/app/ml/model_io.py b/demo5_full_project/app/ml/model_io.py deleted file mode 100644 index 11b195b..0000000 --- a/demo5_full_project/app/ml/model_io.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -import json -from pathlib import Path - -import joblib - - -def save_model(model, path: str, metadata=None) -> None: - path_obj = Path(path) - path_obj.parent.mkdir(parents=True, exist_ok=True) - joblib.dump(model, path_obj) - meta_path = path_obj.with_suffix(path_obj.suffix + ".meta.json") - meta_path.write_text(json.dumps(metadata or {}, ensure_ascii=False, indent=2), encoding="utf-8") - - -def load_model(path: str, return_metadata: bool = False): - path_obj = Path(path) - model = joblib.load(path_obj) - meta_path = path_obj.with_suffix(path_obj.suffix + ".meta.json") - metadata = json.loads(meta_path.read_text(encoding="utf-8")) if meta_path.exists() else {} - if return_metadata: - return model, metadata - return model diff --git a/demo5_full_project/app/ml/tuner.py b/demo5_full_project/app/ml/tuner.py deleted file mode 100644 index 3a71c66..0000000 --- a/demo5_full_project/app/ml/tuner.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -from sklearn.ensemble import RandomForestClassifier -from sklearn.model_selection import cross_val_score - - -def tune_model(X_train, y_train, n_trials: int = 5): - candidate_depths = [2, 3, 4, 5, None] - candidate_estimators = [20, 50, 100, 150, 200] - best_model = None - best_score = -1.0 - - for idx in range(max(1, n_trials)): - model = RandomForestClassifier( - n_estimators=candidate_estimators[idx % len(candidate_estimators)], - max_depth=candidate_depths[idx % len(candidate_depths)], - random_state=42 + idx, - ) - score = float(cross_val_score(model, X_train, y_train, cv=3).mean()) - if score > best_score: - best_score = score - best_model = model - - assert best_model is not None - best_model.fit(X_train, y_train) - return best_model, best_score diff --git a/demo5_full_project/main.py b/demo5_full_project/main.py deleted file mode 100644 index 1918571..0000000 --- a/demo5_full_project/main.py +++ /dev/null @@ -1,75 +0,0 @@ -from __future__ import annotations - -import json -from typing import Iterator, List -from uuid import uuid4 - -from fastapi import FastAPI -from fastapi.responses import StreamingResponse -from pydantic import BaseModel - - -app = FastAPI(title="Demo5 Full Project") -TASKS = {} - - -class ChatRequest(BaseModel): - message: str - - -class TaskCreateRequest(BaseModel): - title: str - priority: str - due_date: str - - -@app.get("/health") -def health(): - return {"status": "ok", "version": "1.0.0"} - - -@app.post("/chat") -def chat(payload: ChatRequest): - message = payload.message.strip() - tools_used: List[str] = [] - if "任务" in message and any(token in message for token in ["列", "list", "所有"]): - tools_used.append("list_tasks") - reply = f"当前共有 {len(TASKS)} 个任务。" - else: - reply = "Demo5 已收到请求,支持任务、流式输出和检索模块演示。" - return {"reply": reply, "tools_used": tools_used} - - -@app.get("/tasks") -def get_tasks(): - return {"tasks": list(TASKS.values())} - - -@app.post("/tasks", status_code=201) -def create_task(payload: TaskCreateRequest): - task = payload.model_dump() - task["id"] = str(uuid4()) - TASKS[task["id"]] = task - return task - - -@app.delete("/tasks/{task_id}") -def delete_task(task_id: str): - if task_id not in TASKS: - from fastapi import HTTPException - - raise HTTPException(status_code=404, detail="Task not found") - del TASKS[task_id] - return {"deleted": True} - - -def _sse_event_stream(message: str) -> Iterator[str]: - for chunk in ["收到消息", "正在处理", message]: - payload = json.dumps({"type": "token", "content": chunk}, ensure_ascii=False) - yield f"data: {payload}\n\n" - yield "data: {\"type\": \"done\"}\n\n" - - -@app.get("/stream") -def stream(message: str): - return StreamingResponse(_sse_event_stream(message), media_type="text/event-stream") From 764f24bfa37cf3f30c1274850647c6d8eb976953 Mon Sep 17 00:00:00 2001 From: aplicity <1634594707@qq.com> Date: Sat, 4 Apr 2026 22:23:48 +0800 Subject: [PATCH 2/4] docs: add learner TODO guide for demo2-starter --- README.md | 12 ++++++++++++ TODO.md | 31 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 TODO.md diff --git a/README.md b/README.md index 1f8e03b..96f5814 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,18 @@ - 通过本关后,push 到 `demo2-starter` 会自动解锁 Demo3 - 完整答案仍然保留在 `main` 分支 +## 学习入口 + +- 先读 [TODO.md](TODO.md) +- 再看 `demo2_task_agent/tests/test_task_agent.py` +- 当前关最重要的三个文件是:`tool_registry.py`、`task_store.py`、`priority_queue.py` + +## 建议实现步骤 + +1. 先实现 `TaskStore`。 +2. 再实现 `PriorityQueue`。 +3. 最后实现 `ToolRegistry` 的权限校验。 + ## 快速导航 - [在线演示](#在线演示) diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..3e4da33 --- /dev/null +++ b/TODO.md @@ -0,0 +1,31 @@ +# Demo2 TODO + +当前目标:补齐任务型 Agent 的基础组件。 + +## 你需要实现的文件 + +- `demo2_task_agent/tool_registry.py` +- `demo2_task_agent/task_store.py` +- `demo2_task_agent/priority_queue.py` + +## 建议实现步骤 + +1. 先实现 `TaskStore.add/get/get_all/update/delete`。 +2. 给任务对象设计一个统一结构,至少包含 `id/title/priority/due_date`。 +3. 实现 `PriorityQueue.push/pop/peek`,确保高优先级先出。 +4. 最后实现 `ToolRegistry.register/call`。 +5. 在 `call()` 里加最小权限判断逻辑。 +6. 跑测试确认三个模块都能协同工作。 + +## 完成标准 + +- `pytest demo2_task_agent/tests/test_task_agent.py -q` 通过 +- 推送到 `demo2-starter` 后,GitHub Actions 成功运行 +- Issues 页面出现 “Demo3 已解锁” + +## 卡住时看哪里 + +- 已完成的 `demo1_cli_agent/` +- 当前分支的 `README.md` +- `docs/demo_specs.md` +- 完整答案在 `main` 分支 From b5e13b132109523d3c1c28350000466e7c83e85b Mon Sep 17 00:00:00 2001 From: aplicity <1634594707@qq.com> Date: Sat, 4 Apr 2026 22:34:23 +0800 Subject: [PATCH 3/4] docs: add hints and checklist for demo2-starter --- CHECKLIST.md | 16 ++++++++++++++++ HINTS.md | 21 +++++++++++++++++++++ README.md | 2 ++ 3 files changed, 39 insertions(+) create mode 100644 CHECKLIST.md create mode 100644 HINTS.md diff --git a/CHECKLIST.md b/CHECKLIST.md new file mode 100644 index 0000000..acc83ef --- /dev/null +++ b/CHECKLIST.md @@ -0,0 +1,16 @@ +# Demo2 CHECKLIST + +## 过关前自检 + +- [ ] `TaskStore.add()` 能返回带 `id` 的任务 +- [ ] `get_all()` 返回列表 +- [ ] `get()` 对不存在任务返回 `None` +- [ ] `update()` 能修改字段 +- [ ] `delete()` 能正确删除任务 +- [ ] `PriorityQueue.peek()` 不移除元素 +- [ ] 高优先级任务先于低优先级弹出 +- [ ] `ToolRegistry.register()` 能登记工具 +- [ ] `call()` 能执行已注册工具 +- [ ] 低权限调用高权限工具时会报错 +- [ ] `pytest demo2_task_agent/tests/test_task_agent.py -q` 通过 +- [ ] push 到 `demo2-starter` 后,Actions 通过 diff --git a/HINTS.md b/HINTS.md new file mode 100644 index 0000000..6b9fb7b --- /dev/null +++ b/HINTS.md @@ -0,0 +1,21 @@ +# Demo2 HINTS + +只给思路,不给答案。 + +## 你可以先想清楚的点 + +- 这一关其实是 3 个独立模块:任务存储、优先级队列、工具注册 +- 先把数据结构设计统一,后面实现会顺很多 +- `ToolRegistry` 的核心不是花哨,而是“注册 + 权限校验 + 调用” + +## 容易卡住的地方 + +- 任务对象字段尽量固定:`id/title/priority/due_date` +- 队列的重点是“高优先级先出来”,不一定非要复杂实现 +- 权限校验不需要设计太重,满足测试即可 + +## 实现顺序建议 + +1. `TaskStore` +2. `PriorityQueue` +3. `ToolRegistry` diff --git a/README.md b/README.md index 96f5814..2757cd4 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ ## 学习入口 - 先读 [TODO.md](TODO.md) +- 卡住时看 [HINTS.md](HINTS.md) +- 提交前对照 [CHECKLIST.md](CHECKLIST.md) - 再看 `demo2_task_agent/tests/test_task_agent.py` - 当前关最重要的三个文件是:`tool_registry.py`、`task_store.py`、`priority_queue.py` From 07997b0e07945da11b71ed40aba218e610a3746a Mon Sep 17 00:00:00 2001 From: aplicity <1634594707@qq.com> Date: Sat, 4 Apr 2026 22:40:04 +0800 Subject: [PATCH 4/4] docs: add faq and reflection for demo2-starter --- FAQ.md | 25 +++++++++++++++++++++++++ README.md | 2 ++ REFLECTION.md | 13 +++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 FAQ.md create mode 100644 REFLECTION.md diff --git a/FAQ.md b/FAQ.md new file mode 100644 index 0000000..4434c66 --- /dev/null +++ b/FAQ.md @@ -0,0 +1,25 @@ +# Demo2 FAQ + +## 1. `TaskStore` 需要上数据库吗? + +不用。 +这一关先把内存存储和数据结构设计清楚更重要。 + +## 2. 优先级队列一定要用堆吗? + +不一定。 +只要能稳定体现“高优先级先出”的行为,先通过测试再优化实现。 + +## 3. 权限校验做多复杂合适? + +这关不需要做企业级权限系统。 +能区分 `read/write/admin` 这类级别就够了。 + +## 4. 为什么有时候三个模块都写了,测试还是不过? + +常见原因是三个模块之间的数据格式不统一。 +先确认任务对象结构是否一致。 + +## 5. 怎么排查 `PriorityQueue` 的顺序问题? + +先只放 2 到 3 个任务手动测试,观察 `peek()` 和 `pop()` 的结果是否符合预期。 diff --git a/README.md b/README.md index 2757cd4..e81abc7 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,9 @@ - 先读 [TODO.md](TODO.md) - 卡住时看 [HINTS.md](HINTS.md) +- 易错点和排查看 [FAQ.md](FAQ.md) - 提交前对照 [CHECKLIST.md](CHECKLIST.md) +- 完成后回看 [REFLECTION.md](REFLECTION.md) - 再看 `demo2_task_agent/tests/test_task_agent.py` - 当前关最重要的三个文件是:`tool_registry.py`、`task_store.py`、`priority_queue.py` diff --git a/REFLECTION.md b/REFLECTION.md new file mode 100644 index 0000000..65535b1 --- /dev/null +++ b/REFLECTION.md @@ -0,0 +1,13 @@ +# Demo2 REFLECTION + +学完这一关,你应该能说清楚这些事: + +- 工具注册表为什么是 Agent 工程化里很重要的一层 +- 为什么需要权限控制,而不是直接随意调用函数 +- 任务 CRUD 和优先级队列如何配合形成任务系统 +- 为什么多模块之间统一数据结构很关键 + +如果你已经完成本关,说明你已经具备: + +- 搭建一个简单任务型 Agent 底座的能力 +- 为后续服务化和路由集成做准备的能力