Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHECKLIST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Demo1 CHECKLIST

## 过关前自检

- [ ] `CLIAgent()` 可以正常创建
- [ ] 初始状态是 `IDLE`
- [ ] `process()` 对普通输入返回字符串
- [ ] `process()` 对空输入不会崩
- [ ] 数学输入能触发计算逻辑
- [ ] `reset()` 后状态恢复为 `IDLE`
- [ ] `python demo1_cli_agent/main.py` 能启动
- [ ] `pytest demo1_cli_agent/tests/test_agent.py -q` 通过
- [ ] push 到 `demo1-starter` 后,Actions 通过
27 changes: 27 additions & 0 deletions FAQ.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Demo1 FAQ

## 1. 为什么 `process()` 一运行就报错?

先确认你是不是还保留了 `NotImplementedError`。
这一关最常见的问题就是只改了一部分逻辑,但忘了去掉占位异常。

## 2. 数学输入一定要做很完整吗?

不用。
先满足测试里最基本的表达式场景,再逐步扩展。

## 3. 为什么测试强调 `process()` 返回字符串?

因为这一关先考察“接口稳定性”。
哪怕还不够智能,也要保证外部调用方始终拿到字符串结果。

## 4. `reset()` 到底要做什么?

至少两件事:

- 清空上下文
- 把状态恢复为 `IDLE`

## 5. 怎么排查多轮对话相关问题?

先打印或观察你保存的 history 结构是否真的在追加,而不是每次都被覆盖。
22 changes: 22 additions & 0 deletions HINTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Demo1 HINTS

只给思路,不给答案。

## 你可以先想清楚的点

- `process()` 的最低要求不是“聪明”,而是“稳定返回字符串”
- 测试最关心的是接口行为,不是类设计得多复杂
- 问候、数学、空输入,这三类先处理好,其他输入再统一兜底

## 容易卡住的地方

- 计算逻辑不需要一开始就支持很复杂的表达式
- `reset()` 不只是清空历史,还要让 `state` 回到 `IDLE`
- 不要让 `process()` 在空字符串时抛异常

## 实现顺序建议

1. 先让类能实例化
2. 再让 `process()` 返回固定字符串
3. 再逐步加意图分支
4. 最后补 CLI 循环
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,32 @@
<strong>Agent</strong> · <strong>ML</strong> · <strong>FastAPI</strong> · <strong>RAG</strong> · <strong>SSE</strong> · <strong>CI Unlock Flow</strong>
</p>

## 当前分支

你现在位于 `demo1-starter`。

- 这个分支是第一关学习骨架,不是完整答案
- 目标是自己实现 `demo1_cli_agent/` 里的核心逻辑
- 当前关卡通过后,push 到 `demo1-starter` 会自动解锁下一关
- 完整参考实现保留在 `main` 分支

## 学习入口

- 先读 [TODO.md](TODO.md)
- 卡住时看 [HINTS.md](HINTS.md)
- 易错点和排查看 [FAQ.md](FAQ.md)
- 提交前对照 [CHECKLIST.md](CHECKLIST.md)
- 完成后回看 [REFLECTION.md](REFLECTION.md)
- 再看 `demo1_cli_agent/tests/test_agent.py`
- 然后补 `demo1_cli_agent/agent.py` 和 `demo1_cli_agent/main.py`

## 建议实现步骤

1. 先让 `CLIAgent` 能被正常实例化。
2. 让 `process()` 对任何输入都返回字符串。
3. 加上简单问候和计算逻辑。
4. 最后补 `reset()` 和 CLI 循环。

## 快速导航

- [在线演示](#在线演示)
Expand Down
13 changes: 13 additions & 0 deletions REFLECTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Demo1 REFLECTION

学完这一关,你应该能说清楚这些事:

- 一个最小可用的 Agent 至少需要输入、状态、上下文和输出
- 状态机为什么适合描述 Agent 的执行过程
- 为什么即使没有 LLM,也能先实现一个规则版 Agent
- 为什么测试优先关注接口稳定性,而不是复杂能力

如果你已经完成本关,说明你已经具备:

- 实现一个最小 CLI Agent 的能力
- 为后续任务型 Agent 打基础的能力
30 changes: 30 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Demo1 TODO

当前目标:完成一个最基础的 CLI Agent。

## 你需要实现的文件

- `demo1_cli_agent/agent.py`
- `demo1_cli_agent/main.py`

## 建议实现步骤

1. 在 `CLIAgent.__init__` 中维护 `state` 和对话历史。
2. 在 `process()` 中处理空输入,保证始终返回字符串。
3. 加入简单意图识别:问候、计算、时间查询。
4. 为数学表达式实现一个最小可用的计算逻辑。
5. 在 `reset()` 中恢复 `IDLE` 状态并清空上下文。
6. 在 `main.py` 中补一个最基本的 CLI 循环。

## 完成标准

- `pytest demo1_cli_agent/tests/test_agent.py -q` 通过
- 推送到 `demo1-starter` 后,GitHub Actions 成功运行
- Issues 页面出现 “Demo2 已解锁”

## 卡住时看哪里

- 当前分支的 `README.md`
- `docs/demo_specs.md`
- `docs/interview_qa.md`
- 完整答案在 `main` 分支
100 changes: 13 additions & 87 deletions demo1_cli_agent/agent.py
Original file line number Diff line number Diff line change
@@ -1,98 +1,24 @@
from __future__ import annotations

from collections import deque
from datetime import datetime
from typing import Deque, Dict, List
import ast
import operator
import re


class SafeCalculator:
_bin_ops = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.Mod: operator.mod,
}
_unary_ops = {
ast.UAdd: operator.pos,
ast.USub: operator.neg,
}

def evaluate(self, expression: str) -> float:
tree = ast.parse(expression, mode="eval")
return float(self._eval_node(tree.body))

def _eval_node(self, node: ast.AST) -> float:
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return float(node.value)
if isinstance(node, ast.BinOp) and type(node.op) in self._bin_ops:
left = self._eval_node(node.left)
right = self._eval_node(node.right)
return self._bin_ops[type(node.op)](left, right)
if isinstance(node, ast.UnaryOp) and type(node.op) in self._unary_ops:
operand = self._eval_node(node.operand)
return self._unary_ops[type(node.op)](operand)
raise ValueError("Unsupported expression")


class CLIAgent:
"""Demo1 starter: implement the basic CLI agent yourself."""

def __init__(self, max_context_len: int = 10):
self.max_context_len = max_context_len
self.state = "IDLE"
self._history: Deque[Dict[str, str]] = deque(maxlen=max_context_len)
self._calculator = SafeCalculator()
self.history = deque(maxlen=max_context_len)

def process(self, user_input: str) -> str:
cleaned = (user_input or "").strip()
self.state = "THINKING"
self._history.append({"role": "user", "content": cleaned})

if not cleaned:
reply = "可以继续发我一个问题,或者让我帮你算一道题。"
elif self._looks_like_math(cleaned):
self.state = "ACTING"
reply = self._handle_math(cleaned)
elif any(token in cleaned.lower() for token in ["时间", "日期", "date", "time"]):
self.state = "ACTING"
reply = f"当前时间是 {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}。"
elif any(token in cleaned.lower() for token in ["你好", "hi", "hello"]):
reply = "你好,我是这个学习项目里的 CLI Agent,可以陪你聊天,也能做简单计算。"
elif cleaned.lower() in {"exit", "quit", "bye"}:
reply = "本轮会话先到这里,随时可以继续。"
else:
reply = "我理解到这是一个普通问题。目前我支持基础对话、时间查询和简单数学计算。"

self.state = "DONE"
self._history.append({"role": "assistant", "content": reply})
return reply
"""
TODO:
1. update agent state
2. record conversation history
3. route simple intents such as greeting / math / datetime
4. return a string reply instead of raising
"""
raise NotImplementedError("Implement CLIAgent.process for Demo1")

def reset(self) -> None:
self._history.clear()
self.state = "IDLE"

def _looks_like_math(self, text: str) -> bool:
if "计算" in text:
return True
return bool(re.search(r"\d+\s*[\+\-\*/%]\s*\d+", text))

def _handle_math(self, text: str) -> str:
match = re.search(r"([-+*/%().\d\s]+)", text)
expression = match.group(1).strip() if match else ""
try:
result = self._calculator.evaluate(expression)
except Exception:
return "这道题我没能正确解析,你可以换成更标准的表达式,例如 1 + 1。"

if result.is_integer():
result_text = str(int(result))
else:
result_text = f"{result:.4f}".rstrip("0").rstrip(".")
return f"计算结果是 {result_text}。"

@property
def history(self) -> List[Dict[str, str]]:
return list(self._history)
"""TODO: clear context and set state back to IDLE."""
raise NotImplementedError("Implement CLIAgent.reset for Demo1")
14 changes: 7 additions & 7 deletions demo1_cli_agent/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@


def main() -> None:
"""
Demo1 starter:
- instantiate CLIAgent
- build a simple REPL loop
- support exit / quit commands
"""
agent = CLIAgent()
print("CLI Agent 已启动,输入 exit 结束。")
while True:
user_input = input(">>> ").strip()
if user_input.lower() in {"exit", "quit"}:
print("Bye.")
break
print(agent.process(user_input))
print("Demo1 starter loaded. Please implement the CLI loop in main.py.")


if __name__ == "__main__":
Expand Down
23 changes: 0 additions & 23 deletions demo2_task_agent/priority_queue.py

This file was deleted.

40 changes: 0 additions & 40 deletions demo2_task_agent/task_store.py

This file was deleted.

40 changes: 0 additions & 40 deletions demo2_task_agent/tool_registry.py

This file was deleted.

Loading