ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,533 @@
|
||||
# KV Cache Demonstration / KV Cache 与错误上下文管理模式
|
||||
|
||||
> Companion material for *AI Agents in Depth*, Chapter 2 — **Experiment 2-3 ★★: Common but harmful context management patterns**.
|
||||
> 配套《深入理解 AI Agent》第 2 章 **实验 2-3 ★★:常见的错误上下文管理模式**。
|
||||
|
||||
← [Chapter 2 index / 返回第 2 章目录](../README.md)
|
||||
|
||||
---
|
||||
|
||||
## English
|
||||
|
||||
### Overview
|
||||
|
||||
A ReAct agent with local filesystem tools that shows how **KV (Key-Value) cache** utilization changes under six implementation patterns—one correct and five incorrect. Small-looking changes can invalidate cache and hurt latency and cost.
|
||||
|
||||
Default model: Moonshot Kimi family (default `kimi-k2.6`), OpenAI tool-calling format.
|
||||
|
||||
> **Model note.** On the live Moonshot endpoint the current Kimi family (`kimi-k2.5` / `kimi-k2.6` / `kimi-k2.7*` / `kimi-k3`) are *reasoning* models: they emit `reasoning_content` and only accept `temperature=1` (handled automatically). They **do** report `cached_tokens`, which this experiment measures. Legacy non-reasoning `moonshot-v1-*` models do **not** report `cached_tokens`, so they cannot demonstrate the cache effect. Default `kimi-k2.6` reports cache hits with a lighter reasoning footprint (less noisy TTFT). Prefer **cache hit rate / cache ratio** as the robust signal; treat TTFT as secondary.
|
||||
|
||||
#### What is KV Cache?
|
||||
|
||||
KV cache stores attention key-value pairs. When conversation context stays stable, cached values can be reused, cutting compute and improving TTFT (Time to First Token).
|
||||
|
||||
### Features
|
||||
|
||||
- ReAct agent with standard OpenAI tool calling
|
||||
- Safe local tools: `read_file`, `find`, `grep`
|
||||
- Robust error handling (tool failures become results)
|
||||
- Six modes: correct + five anti-patterns
|
||||
- Metrics: TTFT, total time, cache hits/misses, tokens
|
||||
- Offline comparison report (`--report`) from saved JSON—no API key
|
||||
- Cost illustration via `--cache-price-ratio`
|
||||
- Detailed logging and smart completion (no tool call → final answer)
|
||||
|
||||
### Implementation modes
|
||||
|
||||
#### 1. Correct (`correct`)
|
||||
Stable context: fixed system prompt, consistent tool order, stable message format, no unnecessary context churn.
|
||||
|
||||
#### 2. Dynamic system prompt (`dynamic_system`)
|
||||
Adds a timestamp to the system prompt every request → full context recreated → cache invalidation, higher TTFT.
|
||||
|
||||
#### 3. Shuffled tools (`shuffled_tools`)
|
||||
Random tool order each request → cache break despite identical functionality.
|
||||
|
||||
#### 4. Dynamic user profile (`dynamic_profile`)
|
||||
Changing user credits in context each iteration → invalidation for irrelevant dynamics (common production anti-pattern).
|
||||
|
||||
#### 5. Sliding window (`sliding_window`)
|
||||
Keeps only the last 5 messages → appears shorter but breaks cache continuity; truncation can backfire.
|
||||
|
||||
#### 6. Text format (`text_format`)
|
||||
History as plain text instead of structured messages → breaks expected format and cache use.
|
||||
|
||||
### Critical implementation detail
|
||||
|
||||
For incorrect modes to properly invalidate KV cache, the **entire message list must be recreated at the start of each iteration**.
|
||||
|
||||
1. **CORRECT:** build messages once; keep appending assistant/tool messages to the same list → stable prefix → cache works.
|
||||
2. **Incorrect modes:** rebuild the full messages list from history at each iteration start (still append tool results *within* an iteration for API correctness) → next turn starts from a recreated list → cache invalidates.
|
||||
|
||||
Both modes append tool results within an iteration so the API sees a complete turn. The difference is whether the list is thrown away and rebuilt at the next iteration boundary.
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# From the repository root: use the shared Chapter 2 environment
|
||||
uv sync --locked --python 3.12 --extra ch2
|
||||
|
||||
# Activate it before changing directories:
|
||||
# macOS/Linux:
|
||||
source .venv/bin/activate
|
||||
# Windows PowerShell: .venv\Scripts\Activate.ps1
|
||||
# Windows cmd: .venv\Scripts\activate.bat
|
||||
|
||||
# pip fallback when uv is not installed:
|
||||
# python -m pip install -e ".[ch2]"
|
||||
|
||||
cd chapter2/kv-cache
|
||||
|
||||
# Single-project compatibility path, still supported during migration:
|
||||
# python -m pip install -r requirements.txt
|
||||
|
||||
cp env.example .env # edit .env with your key
|
||||
# Or export MOONSHOT_API_KEY="your-api-key-here"
|
||||
```
|
||||
|
||||
> **OpenRouter fallback:** If `MOONSHOT_API_KEY` / `KIMI_API_KEY` is unset but `OPENROUTER_API_KEY` is set, the experiment uses OpenRouter (`kimi-*` → `moonshotai/kimi-k2`). With a Moonshot key set, behavior is unchanged.
|
||||
|
||||
### Usage
|
||||
|
||||
#### Interactive mode (default)
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
# Menu: 1–6 modes, 7 Compare All, 0 Exit
|
||||
```
|
||||
|
||||
#### CLI flags
|
||||
|
||||
Chinese `--help`: `python main.py --help`. Key flags:
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--mode MODE` | One strategy: correct / dynamic_system / shuffled_tools / dynamic_profile / sliding_window / text_format |
|
||||
| `--compare` | Run all strategies and print comparison table (needs API key) |
|
||||
| `--report` | **Offline:** build comparison table from saved `result_*.json` / `comparison_*.json` (**no API key**) |
|
||||
| `--input ...` | With `--report`: files / globs / dirs (default: scan cwd) |
|
||||
| `--model MODEL` | Default `kimi-k2.6` (family models that report `cached_tokens` also work) |
|
||||
| `--output PATH` | Result JSON path (default: mode + timestamp) |
|
||||
| `--cache-price-ratio R` | Assumed bill ratio for cached tokens (default `0.1`); illustration only |
|
||||
| `--task`, `--root-dir` | Custom task / filesystem tool root |
|
||||
|
||||
```bash
|
||||
python main.py --mode correct
|
||||
python main.py --mode sliding_window --model kimi-k2.6 --output run.json
|
||||
python main.py --compare
|
||||
python main.py --no-interactive --mode correct
|
||||
```
|
||||
|
||||
#### Offline comparison report (no API key)
|
||||
|
||||
```bash
|
||||
# Uses result_*.json already in this directory
|
||||
python main.py --report
|
||||
|
||||
python main.py --report --input result_correct_*.json result_text_format_*.json
|
||||
python main.py --report --cache-price-ratio 0.5
|
||||
```
|
||||
|
||||
The table compares cache hit rate, cache ratio, TTFT, total time, and illustrative billable-token / savings. Supports legacy `AgentMetrics(...)` string metrics and newer dict-format files.
|
||||
|
||||
> `Bill.Tok` / `Save%` are a transparent function of *measured* tokens and `--cache-price-ratio`—an illustration, not a provider quote.
|
||||
|
||||
#### Custom tasks
|
||||
|
||||
```bash
|
||||
python main.py --mode correct --task "Read all README files and summarize their contents"
|
||||
python main.py --mode correct --root-dir ../.. --task "Analyze the project structure"
|
||||
```
|
||||
|
||||
### Tests and manual checks
|
||||
|
||||
Offline regressions live under `tests/` and do not require API keys:
|
||||
|
||||
```bash
|
||||
uv sync --locked --python 3.12 --extra ch2 --extra dev
|
||||
source .venv/bin/activate
|
||||
cd chapter2/kv-cache
|
||||
python -m pytest tests
|
||||
```
|
||||
|
||||
Live smoke checks and demonstrations live under `tests/manual/`. They are not
|
||||
collected by pytest because their filenames use `check_*.py` or `demo_*.py`:
|
||||
|
||||
```bash
|
||||
MOONSHOT_API_KEY="your-key" python tests/manual/demo_quick.py
|
||||
MOONSHOT_API_KEY="your-key" python tests/manual/check_tool_calling.py
|
||||
MOONSHOT_API_KEY="your-key" python tests/manual/check_cache_invalidation.py
|
||||
MOONSHOT_API_KEY="your-key" python tests/manual/check_agent_error_recovery.py
|
||||
```
|
||||
|
||||
### Metrics
|
||||
|
||||
**Performance:** TTFT (per iteration; cold start vs with cache), average TTFT, improvement %, total time, iterations, tool calls.
|
||||
|
||||
**Cache:** cached tokens, hits, misses, hit rate.
|
||||
|
||||
**Tokens:** prompt / completion tokens; **cache ratio** = share of prompt tokens served from cache.
|
||||
|
||||
### Expected results
|
||||
|
||||
1. **Correct:** after the first iteration, cached tokens appear; steady TTFT; stable prefix served from cache.
|
||||
2. **Incorrect:** collapsing **cache ratio** (e.g. shuffled tools can drop ratio to ~1/3 of correct); higher TTFT (text_format / shuffle can more than double first-token latency); longer total time (up to ~2.4× for `text_format` in measured runs).
|
||||
|
||||
> On reasoning models TTFT has extra variance from hidden thinking—**cache ratio** is the cleanest evidence. Appending dynamic data at the *end* of an otherwise-stable prefix (`dynamic_system` / `dynamic_profile`) only invalidates *from that point*—base prefix may still cache, so headline cache ratio can look close to `correct` while total time still regresses. Keep dynamic data out of the prefix entirely.
|
||||
|
||||
### Example output
|
||||
|
||||
```
|
||||
📊 Performance Metrics:
|
||||
• Time to First Token (TTFT): 0.823 seconds
|
||||
• TTFT per iteration:
|
||||
Iteration 1: 0.823s
|
||||
Iteration 2: 0.234s (with cache)
|
||||
...
|
||||
• TTFT Analysis:
|
||||
First iteration: 0.823s
|
||||
Last iteration: 0.192s
|
||||
Average (after first): 0.203s
|
||||
Improvement: 76.7%
|
||||
```
|
||||
|
||||
### Comparison table (`--report` on saved results)
|
||||
|
||||
Real numbers from one `--compare` run (`kimi-k2.6`, root = this folder, task = find Python files / read `main.py` + `agent.py` / summarize in 3 sentences), then `python main.py --report`:
|
||||
|
||||
```
|
||||
Mode Iters 1st TTFT Avg TTFT Total(s) Prompt Cached Hit% Cache% Bill.Tok Save%
|
||||
----------------------------------------------------------------------------------------------------------------
|
||||
correct 3 2.328 6.054 18.163 7,567 768 100.0 10.1 6,876 9.1
|
||||
dynamic_profile 3 2.206 5.986 17.962 7,652 768 100.0 10.0 6,961 9.0
|
||||
dynamic_system 3 2.497 8.085 24.260 7,639 768 100.0 10.1 6,948 9.0
|
||||
shuffled_tools 3 7.818 11.122 33.369 7,568 256 100.0 3.4 7,338 3.0
|
||||
sliding_window 5 2.234 3.649 14.704 2,224 1,510 100.0 67.9 865 61.1
|
||||
text_format 3 6.189 14.432 43.297 7,430 674 100.0 9.1 6,823 8.2
|
||||
```
|
||||
|
||||
Reading: `shuffled_tools` reorders tool definitions near the front of the prefix → **cache ratio 10.1% → 3.4%**, first TTFT ~2.3s → ~7.8s. `text_format` ~**2.4×** correct total time. `sliding_window` high ratio only because the prompt is truncated (high ratio on a tiny prompt ≠ efficient run).
|
||||
|
||||
`Cache%` = share of prompt tokens from cache; `Hit%` = share of *iterations* with any cache. Regenerate with `python main.py --compare`.
|
||||
|
||||
### Key insights
|
||||
|
||||
1. Stable context is critical
|
||||
2. Order matters (even reordering identical content breaks cache)
|
||||
3. Avoid dynamic metadata in the prefix
|
||||
4. Use the API’s structured message format
|
||||
5. Full history often beats aggressive truncation for cache
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
kv-cache/
|
||||
├── agent.py # ReAct agent + modes
|
||||
├── main.py # Experiment runner CLI
|
||||
├── tests/ # offline pytest regressions
|
||||
│ └── manual/ # live/API smoke checks, not collected by pytest
|
||||
├── requirements.txt
|
||||
├── README.md
|
||||
├── result_*.json # retained receipts for offline --report
|
||||
└── kv_cache_demo.log
|
||||
```
|
||||
|
||||
**Components:** `KVCacheAgent`, `LocalFileTools`, `KVCacheMode`, `AgentMetrics`.
|
||||
|
||||
**Error handling:** tool/arg errors returned as results; continue on failure; deny paths outside root.
|
||||
|
||||
### Advanced configuration
|
||||
|
||||
```bash
|
||||
export MOONSHOT_API_KEY="your-key"
|
||||
export LOG_LEVEL="DEBUG" # INFO, WARNING, ERROR
|
||||
```
|
||||
|
||||
Extend `KVCacheMode` in `agent.py` and implement `_get_system_prompt()` / `_get_tools()` / `_format_messages()`.
|
||||
|
||||
### Best practices
|
||||
|
||||
1. Keep system prompts stable
|
||||
2. Keep tool order fixed
|
||||
3. Avoid counters/credits/timestamps in the prefix
|
||||
4. Use proper message structure
|
||||
5. Prefer continuity over aggressive truncation
|
||||
6. Design context with caching in mind
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- **High TTFT in correct mode:** first request cold start; check key/network/model
|
||||
- **Zero cache hits:** use current Kimi models that report `cached_tokens` (not `moonshot-v1-*`); verify context stability
|
||||
- **Tool errors:** permissions, paths within root, files exist
|
||||
|
||||
### References
|
||||
|
||||
- [Kimi API Documentation](https://platform.moonshot.cn/docs/api-reference)
|
||||
- [ReAct Pattern Paper](https://arxiv.org/abs/2210.03629)
|
||||
- [Transformer KV Cache Explanation](https://huggingface.co/docs/transformers/kv_cache)
|
||||
|
||||
---
|
||||
|
||||
## 中文
|
||||
|
||||
### 概述
|
||||
|
||||
用带本地文件系统工具的 ReAct Agent,展示 **KV(Key-Value)Cache** 在六种实现模式(一种正确、五种错误)下的利用率差异。看似无害的改动可能让缓存失效,显著拖慢延迟并推高成本。
|
||||
|
||||
默认模型:Moonshot Kimi 系列(默认 `kimi-k2.6`),标准 OpenAI 工具调用格式。
|
||||
|
||||
> **模型说明。** 线上 Moonshot 当前 Kimi 族(`kimi-k2.5` / `kimi-k2.6` / `kimi-k2.7*` / `kimi-k3`)均为*推理*模型:会吐 `reasoning_content`,且只接受 `temperature=1`(代码已自动处理)。它们**会上报** `cached_tokens`,本实验正依赖此指标。旧版非推理 `moonshot-v1-*` **不上报** `cached_tokens`,无法展示缓存效应。默认 `kimi-k2.6` 能报缓存命中且推理开销较轻(TTFT 噪声更小)。请以 **缓存命中率 / 缓存比例** 为稳健信号,TTFT 作辅助。
|
||||
|
||||
#### 什么是 KV Cache?
|
||||
|
||||
KV Cache 存储注意力机制中的键值对。对话上下文稳定时,可复用这些缓存值,显著减少计算并改善 TTFT(首 token 延迟)。
|
||||
|
||||
### 功能
|
||||
|
||||
- ReAct Agent + 标准 OpenAI 工具调用
|
||||
- 安全本地工具:`read_file`、`find`、`grep`
|
||||
- 工具失败以结果回传并继续执行
|
||||
- 六种模式:正确 + 五种反模式
|
||||
- 指标:TTFT、总时间、缓存命中/未命中、token
|
||||
- 离线对比报告(`--report`)从已保存 JSON 生成——无需 API Key
|
||||
- 通过 `--cache-price-ratio` 做成本示意
|
||||
- 详细日志与智能收尾(无工具调用即视为最终答案)
|
||||
|
||||
### 实现模式
|
||||
|
||||
#### 1. 正确实现(`correct`)
|
||||
全程稳定上下文:固定系统提示、一致工具顺序、稳定消息格式、无无谓上下文改动。
|
||||
|
||||
#### 2. 动态系统提示(`dynamic_system`)
|
||||
每次请求在系统提示中加时间戳 → 整表重建 → 缓存失效、TTFT 上升。
|
||||
|
||||
#### 3. 打乱工具列表(`shuffled_tools`)
|
||||
每次随机工具顺序 → 功能相同仍破坏缓存。
|
||||
|
||||
#### 4. 动态用户资料(`dynamic_profile`)
|
||||
每轮把变化的用户额度塞进上下文 → 无关动态导致失效(常见生产反模式)。
|
||||
|
||||
#### 5. 滑动窗口(`sliding_window`)
|
||||
只保留最近 5 条消息 → 看似更短,实则打断缓存连续性。
|
||||
|
||||
#### 6. 纯文本格式(`text_format`)
|
||||
历史写成纯文本而非结构化消息 → 破坏约定格式与缓存。
|
||||
|
||||
### 关键实现细节
|
||||
|
||||
错误模式要**真正**使 KV Cache 失效,必须在**每一轮迭代开始时**整表重建 messages。
|
||||
|
||||
1. **CORRECT:** 首次构建 messages,之后只在同一列表上追加 assistant/tool → 前缀稳定 → 缓存生效。
|
||||
2. **错误模式:** 每轮开始从对话历史重建整个 messages(轮内仍追加 tool 结果以保证 API 流正确)→ 下一轮从新列表开始 → 缓存失效。
|
||||
|
||||
两模式在轮内都会追加 tool 结果;差异在于下一轮是否丢弃并重建列表。
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
# 在仓库根目录使用统一的第 2 章环境
|
||||
uv sync --locked --python 3.12 --extra ch2
|
||||
|
||||
# 切换目录前先激活环境:
|
||||
# macOS/Linux:
|
||||
source .venv/bin/activate
|
||||
# Windows PowerShell:.venv\Scripts\Activate.ps1
|
||||
# Windows cmd:.venv\Scripts\activate.bat
|
||||
|
||||
# 未安装 uv 时可用 pip 兜底:
|
||||
# python -m pip install -e ".[ch2]"
|
||||
|
||||
cd chapter2/kv-cache
|
||||
|
||||
# 迁移期间仍支持单项目兼容路径:
|
||||
# python -m pip install -r requirements.txt
|
||||
|
||||
cp env.example .env # 编辑 .env,填入 API Key
|
||||
# 或 export MOONSHOT_API_KEY="your-api-key-here"
|
||||
```
|
||||
|
||||
> **通用回退(OpenRouter)**:未设置 `MOONSHOT_API_KEY` / `KIMI_API_KEY` 时,只要配置了 `OPENROUTER_API_KEY`,实验会自动改走 OpenRouter(`kimi-*` 会映射为 `moonshotai/kimi-k2`)。设置了 Moonshot key 时行为完全不变。
|
||||
|
||||
### 用法
|
||||
|
||||
#### 交互模式(默认)
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
# 菜单:1–6 模式,7 全部对比,0 退出
|
||||
```
|
||||
|
||||
#### 命令行参数
|
||||
|
||||
中文 `--help`:`python main.py --help`。主要参数:
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `--mode MODE` | 单个策略(correct / dynamic_system / shuffled_tools / dynamic_profile / sliding_window / text_format) |
|
||||
| `--compare` | 依次运行全部策略并打印横向对比表(需要 API Key) |
|
||||
| `--report` | **离线**:从已保存的 `result_*.json` / `comparison_*.json` 生成对比表,**无需 API Key** |
|
||||
| `--input ...` | 配合 `--report` 指定结果文件 / 通配符 / 目录(默认扫描当前目录) |
|
||||
| `--model MODEL` | 默认 `kimi-k2.6`(会上报 `cached_tokens` 的同族模型亦可) |
|
||||
| `--output PATH` | 结果 JSON 路径(默认按模式 + 时间戳命名) |
|
||||
| `--cache-price-ratio R` | 缓存 token 相对正常 token 的计费比例示意(默认 `0.1`) |
|
||||
| `--task`, `--root-dir` | 自定义任务 / 文件工具根目录 |
|
||||
|
||||
```bash
|
||||
python main.py --mode correct
|
||||
python main.py --mode sliding_window --model kimi-k2.6 --output run.json
|
||||
python main.py --compare
|
||||
python main.py --no-interactive --mode correct
|
||||
```
|
||||
|
||||
#### 离线对比报告(无需 API Key)
|
||||
|
||||
```bash
|
||||
python main.py --report
|
||||
python main.py --report --input result_correct_*.json result_text_format_*.json
|
||||
python main.py --report --cache-price-ratio 0.5
|
||||
```
|
||||
|
||||
对比表包含缓存命中率、缓存比例、TTFT、总时间,以及示意性的可计费 token / 节省比例。兼容旧版 `AgentMetrics(...)` 字符串指标与新版 dict 格式。
|
||||
|
||||
> `Bill.Tok` / `Save%` 是实测 token 与 `--cache-price-ratio` 的透明函数——仅作成本示意,非某一厂商报价。
|
||||
|
||||
#### 自定义任务
|
||||
|
||||
```bash
|
||||
python main.py --mode correct --task "Read all README files and summarize their contents"
|
||||
python main.py --mode correct --root-dir ../.. --task "Analyze the project structure"
|
||||
```
|
||||
|
||||
### 测试与手动检查
|
||||
|
||||
离线回归测试位于 `tests/`,不需要 API Key:
|
||||
|
||||
```bash
|
||||
uv sync --locked --python 3.12 --extra ch2 --extra dev
|
||||
source .venv/bin/activate
|
||||
cd chapter2/kv-cache
|
||||
python -m pytest tests
|
||||
```
|
||||
|
||||
需要真实 API 的冒烟脚本和演示位于 `tests/manual/`。这些文件使用
|
||||
`check_*.py` 或 `demo_*.py` 命名,因此不会被 pytest 默认收集:
|
||||
|
||||
```bash
|
||||
MOONSHOT_API_KEY="your-key" python tests/manual/demo_quick.py
|
||||
MOONSHOT_API_KEY="your-key" python tests/manual/check_tool_calling.py
|
||||
MOONSHOT_API_KEY="your-key" python tests/manual/check_cache_invalidation.py
|
||||
MOONSHOT_API_KEY="your-key" python tests/manual/check_agent_error_recovery.py
|
||||
```
|
||||
|
||||
### 指标说明
|
||||
|
||||
**性能:** TTFT(按迭代;冷启动 vs 有缓存)、平均 TTFT、改善百分比、总时间、迭代次数、工具调用次数。
|
||||
|
||||
**缓存:** 缓存 token、命中、未命中、命中率。
|
||||
|
||||
**Token:** 提示 / 补全 token;**缓存比例** = 来自缓存的 prompt token 占比。
|
||||
|
||||
### 预期结果
|
||||
|
||||
1. **正确实现:** 第一轮之后上报缓存 token;TTFT 更稳;稳定前缀由缓存服务。
|
||||
2. **错误实现:** **缓存比例**崩塌(如打乱工具可将比例降到正确模式约 1/3);TTFT 更高(text_format / 打乱工具可让首 token 延迟翻倍以上);总时间更长(实测 `text_format` 可达约 2.4×)。
|
||||
|
||||
> 推理模型上 TTFT 因隐藏思考 token 方差更大——**缓存比例**是最干净的证据。把动态数据接在**已稳定前缀末尾**(`dynamic_system` / `dynamic_profile`)只会从改动点起失效——前缀前半仍可能命中,因此标题缓存比例可能接近 `correct`,但总时间仍变差。教训:动态数据不要进入前缀。
|
||||
|
||||
### 示例输出
|
||||
|
||||
```
|
||||
📊 Performance Metrics:
|
||||
• Time to First Token (TTFT): 0.823 seconds
|
||||
• TTFT per iteration:
|
||||
Iteration 1: 0.823s
|
||||
Iteration 2: 0.234s (with cache)
|
||||
...
|
||||
• TTFT Analysis:
|
||||
First iteration: 0.823s
|
||||
Last iteration: 0.192s
|
||||
Average (after first): 0.203s
|
||||
Improvement: 76.7%
|
||||
```
|
||||
|
||||
### 对比表(对本目录已保存结果执行 `--report`)
|
||||
|
||||
一次 `--compare` 真实测得(`kimi-k2.6`,根目录为本文件夹,任务为查找 Python 文件 / 读 `main.py` + `agent.py` / 用 3 句话总结),再 `python main.py --report`:
|
||||
|
||||
```
|
||||
Mode Iters 1st TTFT Avg TTFT Total(s) Prompt Cached Hit% Cache% Bill.Tok Save%
|
||||
----------------------------------------------------------------------------------------------------------------
|
||||
correct 3 2.328 6.054 18.163 7,567 768 100.0 10.1 6,876 9.1
|
||||
dynamic_profile 3 2.206 5.986 17.962 7,652 768 100.0 10.0 6,961 9.0
|
||||
dynamic_system 3 2.497 8.085 24.260 7,639 768 100.0 10.1 6,948 9.0
|
||||
shuffled_tools 3 7.818 11.122 33.369 7,568 256 100.0 3.4 7,338 3.0
|
||||
sliding_window 5 2.234 3.649 14.704 2,224 1,510 100.0 67.9 865 61.1
|
||||
text_format 3 6.189 14.432 43.297 7,430 674 100.0 9.1 6,823 8.2
|
||||
```
|
||||
|
||||
解读:`shuffled_tools` 打乱靠前的工具定义 → **缓存比例 10.1% → 3.4%**,首 TTFT 约 2.3s → 7.8s。`text_format` 总时间约 **2.4×**。`sliding_window` 高比例只因提示词被截断(小提示词上的高比例 ≠ 高效运行)。
|
||||
|
||||
`Cache%` = 来自缓存的 prompt token 占比;`Hit%` = 出现任意缓存的*迭代*占比。用 `python main.py --compare` 重跑复现。
|
||||
|
||||
### 关键洞察
|
||||
|
||||
1. 稳定上下文至关重要
|
||||
2. 顺序也重要(相同内容重排也会破缓存)
|
||||
3. 前缀中避免动态元数据
|
||||
4. 使用 API 期望的结构化消息格式
|
||||
5. 完整历史往往比激进截断更利于缓存
|
||||
|
||||
### 架构
|
||||
|
||||
```
|
||||
kv-cache/
|
||||
├── agent.py # ReAct Agent + 各模式
|
||||
├── main.py # 实验入口 CLI
|
||||
├── tests/ # 离线 pytest 回归测试
|
||||
│ └── manual/ # 真实 API 冒烟脚本,不被 pytest 收集
|
||||
├── requirements.txt
|
||||
├── README.md
|
||||
├── result_*.json # 支持离线 --report 的保留结果
|
||||
└── kv_cache_demo.log
|
||||
```
|
||||
|
||||
**组件:** `KVCacheAgent`、`LocalFileTools`、`KVCacheMode`、`AgentMetrics`。
|
||||
|
||||
**错误处理:** 工具/参数错误以结果回传;失败后继续;拒绝根目录外访问。
|
||||
|
||||
### 进阶配置
|
||||
|
||||
```bash
|
||||
export MOONSHOT_API_KEY="your-key"
|
||||
export LOG_LEVEL="DEBUG" # INFO, WARNING, ERROR
|
||||
```
|
||||
|
||||
可在 `agent.py` 扩展 `KVCacheMode`,并实现 `_get_system_prompt()` / `_get_tools()` / `_format_messages()`。
|
||||
|
||||
### KV Cache 最佳实践
|
||||
|
||||
1. 系统提示保持稳定
|
||||
2. 工具顺序固定
|
||||
3. 前缀中避免计数器/额度/时间戳
|
||||
4. 使用正确的消息结构
|
||||
5. 优先保持连续性,慎用激进截断
|
||||
6. 从设计上考虑缓存友好
|
||||
|
||||
### 故障排除
|
||||
|
||||
- **正确模式 TTFT 仍高:** 首请求冷启动;检查 key / 网络 / 模型
|
||||
- **缓存命中为零:** 使用会上报 `cached_tokens` 的当前 Kimi 模型(非 `moonshot-v1-*`);确认上下文确实稳定
|
||||
- **工具执行错误:** 权限、路径在 root 内、文件可读
|
||||
|
||||
### 参考
|
||||
|
||||
- [Kimi API Documentation](https://platform.moonshot.cn/docs/api-reference)
|
||||
- [ReAct Pattern Paper](https://arxiv.org/abs/2210.03629)
|
||||
- [Transformer KV Cache Explanation](https://huggingface.co/docs/transformers/kv_cache)
|
||||
|
||||
---
|
||||
|
||||
## Notes / 说明
|
||||
|
||||
- Saved `result_*.json` in this directory enable offline `--report` without spending API quota.
|
||||
- 本目录随附的 `result_*.json` 支持离线 `--report`,无需消耗 API 额度。
|
||||
@@ -0,0 +1,888 @@
|
||||
"""
|
||||
KV Cache Demonstration Agent with ReAct Pattern
|
||||
Demonstrates the importance of KV cache through correct and incorrect implementations.
|
||||
Uses local file system tools to read and search through code files.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
import random
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from enum import Enum
|
||||
from datetime import datetime
|
||||
from openai import OpenAI
|
||||
import glob as glob_module
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def _is_reasoning_model(model) -> bool:
|
||||
"""True for models that emit reasoning_content and only accept temperature=1.
|
||||
|
||||
On the live Moonshot endpoint the whole current Kimi family reasons:
|
||||
kimi-k2.5 / kimi-k2.6 / kimi-k2.7* / kimi-k3. The legacy moonshot-v1-*
|
||||
chat models do NOT reason (and also do not report cached_tokens)."""
|
||||
m = str(model or "").lower().replace("/", "-")
|
||||
if "gpt-5" in m:
|
||||
return True
|
||||
return any(tag in m for tag in ("kimi-k2.5", "kimi-k2.6", "kimi-k2.7", "kimi-k3"))
|
||||
|
||||
|
||||
def _reasoning_safe_temperature(model, requested=1.0):
|
||||
"""Reasoning models (Kimi K2.5/K2.6/K2.7/K3, GPT-5, ...) only accept
|
||||
temperature=1. Return 1 for those; otherwise the requested value so
|
||||
non-reasoning providers (moonshot-v1, Doubao, DeepSeek) are unchanged."""
|
||||
return 1 if _is_reasoning_model(model) else requested
|
||||
|
||||
|
||||
def _reasoning_safe_max_tokens(model, requested=2000):
|
||||
"""Reasoning models spend completion budget on hidden reasoning tokens
|
||||
before emitting content / tool calls. Give them enough headroom so a
|
||||
tool call is not truncated away; leave non-reasoning models unchanged."""
|
||||
return max(requested, 4096) if _is_reasoning_model(model) else requested
|
||||
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KVCacheMode(Enum):
|
||||
"""Different KV cache optimization modes"""
|
||||
CORRECT = "correct" # Correct implementation with stable context
|
||||
DYNAMIC_SYSTEM = "dynamic_system" # Changing system prompt with timestamp
|
||||
SHUFFLED_TOOLS = "shuffled_tools" # Shuffling tool order each request
|
||||
DYNAMIC_PROFILE = "dynamic_profile" # Changing user profile with credits
|
||||
SLIDING_WINDOW = "sliding_window" # Only keeping recent 6 messages
|
||||
TEXT_FORMAT = "text_format" # Formatting messages as plain text
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolCall:
|
||||
"""Represents a single tool call"""
|
||||
name: str
|
||||
arguments: Dict[str, Any]
|
||||
result: Any = None
|
||||
error: Optional[str] = None
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentMetrics:
|
||||
"""Metrics for agent performance"""
|
||||
ttft: float = 0.0 # Time to first token (first iteration)
|
||||
ttft_per_iteration: List[float] = field(default_factory=list) # TTFT for each iteration
|
||||
total_time: float = 0.0
|
||||
iterations: int = 0
|
||||
tool_calls: int = 0
|
||||
cache_hits: int = 0
|
||||
cache_misses: int = 0
|
||||
prompt_tokens: int = 0
|
||||
completion_tokens: int = 0
|
||||
cached_tokens: int = 0
|
||||
|
||||
|
||||
class LocalFileTools:
|
||||
"""Local implementations of file system tools"""
|
||||
|
||||
def __init__(self, root_dir: str = "."):
|
||||
self.root_dir = os.path.abspath(root_dir)
|
||||
logger.info(f"File tools initialized with root: {self.root_dir}")
|
||||
|
||||
def read_file(self, file_path: str, offset: int = 0, size: int = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Read contents of a file
|
||||
|
||||
Args:
|
||||
file_path: Path to the file relative to root directory
|
||||
offset: Line number to start reading from (0-based, default: 0)
|
||||
size: Number of lines to read (default: None, read all)
|
||||
|
||||
Returns:
|
||||
Dictionary with file contents or error
|
||||
"""
|
||||
try:
|
||||
full_path = os.path.join(self.root_dir, file_path)
|
||||
|
||||
# Security check - ensure path is within root_dir
|
||||
real_path = os.path.realpath(full_path)
|
||||
if not real_path.startswith(self.root_dir):
|
||||
return {
|
||||
"error": f"Access denied: Path outside root directory",
|
||||
"success": False
|
||||
}
|
||||
|
||||
with open(real_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
total_lines = len(lines)
|
||||
|
||||
# Apply offset and size
|
||||
if offset < 0:
|
||||
offset = 0
|
||||
if offset >= total_lines:
|
||||
return {
|
||||
"path": file_path,
|
||||
"content": "",
|
||||
"total_lines": total_lines,
|
||||
"lines_read": 0,
|
||||
"offset": offset,
|
||||
"success": True,
|
||||
"message": f"Offset {offset} exceeds file length ({total_lines} lines)"
|
||||
}
|
||||
|
||||
# Determine end line
|
||||
if size is None or size < 0:
|
||||
# Negative size is a common "read all" sentinel; avoid lines[i:-n].
|
||||
end = total_lines
|
||||
else:
|
||||
end = min(offset + size, total_lines)
|
||||
|
||||
# Get the requested lines
|
||||
selected_lines = lines[offset:end]
|
||||
content = ''.join(selected_lines)
|
||||
|
||||
# Apply size limit for safety (10KB)
|
||||
truncated = False
|
||||
if len(content) > 10000:
|
||||
content = content[:10000]
|
||||
truncated = True
|
||||
|
||||
return {
|
||||
"path": file_path,
|
||||
"content": content,
|
||||
"total_lines": total_lines,
|
||||
"lines_read": len(selected_lines),
|
||||
"offset": offset,
|
||||
"end_line": end,
|
||||
"truncated": truncated,
|
||||
"success": True
|
||||
}
|
||||
except FileNotFoundError:
|
||||
return {
|
||||
"error": f"File not found: {file_path}",
|
||||
"success": False
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"error": f"Error reading file: {str(e)}",
|
||||
"success": False
|
||||
}
|
||||
|
||||
def find(self, pattern: str = "*", directory: str = ".") -> Dict[str, Any]:
|
||||
"""
|
||||
Find files matching a pattern (similar to Unix find command)
|
||||
|
||||
Args:
|
||||
pattern: File name pattern (supports wildcards, default: "*" for all files)
|
||||
directory: Directory to search in (relative to root_dir)
|
||||
|
||||
Returns:
|
||||
Dictionary with list of matching files
|
||||
"""
|
||||
try:
|
||||
# Handle directory path properly
|
||||
if directory == ".":
|
||||
search_dir = self.root_dir
|
||||
else:
|
||||
# Remove leading/trailing slashes for consistency
|
||||
directory = directory.strip('/')
|
||||
search_dir = os.path.join(self.root_dir, directory)
|
||||
|
||||
# Security check
|
||||
real_path = os.path.realpath(search_dir)
|
||||
if not real_path.startswith(self.root_dir):
|
||||
return {
|
||||
"error": f"Access denied: Path outside root directory",
|
||||
"success": False
|
||||
}
|
||||
|
||||
# Check if directory exists
|
||||
if not os.path.exists(real_path):
|
||||
return {
|
||||
"error": f"Directory not found: {directory}",
|
||||
"success": False
|
||||
}
|
||||
|
||||
# Use glob to find matching files
|
||||
matches = []
|
||||
for root, dirs, files in os.walk(real_path):
|
||||
# Filter hidden directories and __pycache__
|
||||
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
|
||||
|
||||
for file in files:
|
||||
# Skip hidden files and .pyc files
|
||||
if file.startswith('.') or file.endswith('.pyc'):
|
||||
continue
|
||||
|
||||
if glob_module.fnmatch.fnmatch(file, pattern):
|
||||
# Get path relative to root_dir (not search_dir)
|
||||
full_path = os.path.join(root, file)
|
||||
rel_path = os.path.relpath(full_path, self.root_dir)
|
||||
matches.append(rel_path)
|
||||
|
||||
# Sort for consistency
|
||||
matches.sort()
|
||||
|
||||
# Limit results for demonstration
|
||||
if len(matches) > 100:
|
||||
matches = matches[:100]
|
||||
truncated = True
|
||||
else:
|
||||
truncated = False
|
||||
|
||||
return {
|
||||
"pattern": pattern,
|
||||
"directory": directory,
|
||||
"matches": matches,
|
||||
"count": len(matches),
|
||||
"truncated": truncated,
|
||||
"success": True
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"error": f"Error finding files: {str(e)}",
|
||||
"success": False
|
||||
}
|
||||
|
||||
def grep(self, pattern: str, file_path: str = None, directory: str = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Search for pattern in files (similar to Unix grep command)
|
||||
|
||||
Args:
|
||||
pattern: Regular expression pattern to search for
|
||||
file_path: Single file to search in (optional)
|
||||
directory: Directory to search in (optional)
|
||||
|
||||
Returns:
|
||||
Dictionary with matching lines
|
||||
"""
|
||||
try:
|
||||
matches = []
|
||||
files_searched = []
|
||||
|
||||
if file_path:
|
||||
# Search in single file
|
||||
full_path = os.path.join(self.root_dir, file_path)
|
||||
real_path = os.path.realpath(full_path)
|
||||
|
||||
if not real_path.startswith(self.root_dir):
|
||||
return {
|
||||
"error": f"Access denied: Path outside root directory",
|
||||
"success": False
|
||||
}
|
||||
|
||||
files_to_search = [file_path]
|
||||
elif directory:
|
||||
# Search in directory
|
||||
search_dir = os.path.join(self.root_dir, directory)
|
||||
real_path = os.path.realpath(search_dir)
|
||||
|
||||
if not real_path.startswith(self.root_dir):
|
||||
return {
|
||||
"error": f"Access denied: Path outside root directory",
|
||||
"success": False
|
||||
}
|
||||
|
||||
# Find all text files in directory
|
||||
files_to_search = []
|
||||
for root, dirs, files in os.walk(real_path):
|
||||
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
||||
for file in files:
|
||||
if file.endswith(('.py', '.txt', '.md', '.json', '.yaml', '.yml', '.js', '.ts', '.jsx', '.tsx')):
|
||||
rel_path = os.path.relpath(os.path.join(root, file), self.root_dir)
|
||||
files_to_search.append(rel_path)
|
||||
if len(files_to_search) >= 50: # Limit files for demonstration
|
||||
break
|
||||
else:
|
||||
return {
|
||||
"error": "Must specify either file_path or directory",
|
||||
"success": False
|
||||
}
|
||||
|
||||
# Compile regex pattern
|
||||
regex = re.compile(pattern, re.IGNORECASE)
|
||||
|
||||
# Search in files
|
||||
for file in files_to_search:
|
||||
full_path = os.path.join(self.root_dir, file)
|
||||
try:
|
||||
with open(full_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
lines = f.readlines()
|
||||
for i, line in enumerate(lines, 1):
|
||||
if regex.search(line):
|
||||
matches.append({
|
||||
"file": file,
|
||||
"line_num": i,
|
||||
"line": line.strip()[:200] # Truncate long lines
|
||||
})
|
||||
if len(matches) >= 100: # Limit matches
|
||||
break
|
||||
files_searched.append(file)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if len(matches) >= 100:
|
||||
break
|
||||
|
||||
return {
|
||||
"pattern": pattern,
|
||||
"matches": matches,
|
||||
"files_searched": len(files_searched),
|
||||
"match_count": len(matches),
|
||||
"truncated": len(matches) >= 100,
|
||||
"success": True
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"error": f"Error searching: {str(e)}",
|
||||
"success": False
|
||||
}
|
||||
|
||||
|
||||
class KVCacheAgent:
|
||||
"""
|
||||
ReAct Agent with different KV cache optimization modes
|
||||
"""
|
||||
|
||||
def __init__(self, api_key: str, mode: KVCacheMode = KVCacheMode.CORRECT,
|
||||
model: str = "kimi-k2.6", root_dir: str = ".",
|
||||
verbose: bool = True):
|
||||
"""
|
||||
Initialize the agent
|
||||
|
||||
Args:
|
||||
api_key: API key for Moonshot/Kimi
|
||||
mode: KV cache optimization mode
|
||||
model: Model to use
|
||||
root_dir: Root directory for file operations
|
||||
verbose: If True, log detailed information
|
||||
"""
|
||||
# 默认走 Moonshot/Kimi 官方端点;若传入的是 OpenRouter key(sk-or-…),
|
||||
# 则自动回退到 OpenRouter,并把 kimi-* 模型名映射为 moonshotai/kimi-k2。
|
||||
# 端点、key 与模型名映射统一由 agentbook 的 provider 注册表维护;
|
||||
# “这把 key 属于谁”只有调用方知道,因此在此处判定后再交给注册表解析。
|
||||
from agentbook.providers import is_openrouter_key, resolve_backend
|
||||
|
||||
provider = "openrouter" if is_openrouter_key(api_key) else "kimi"
|
||||
backend = resolve_backend(provider, model=model, api_key=api_key)
|
||||
self.client = OpenAI(
|
||||
api_key=backend.api_key,
|
||||
base_url=backend.base_url
|
||||
)
|
||||
self.model = backend.model
|
||||
self.mode = mode
|
||||
self.verbose = verbose
|
||||
self.tools = LocalFileTools(root_dir)
|
||||
|
||||
# Initialize conversation history
|
||||
self.conversation_history = []
|
||||
self.user_credits = 100 # For dynamic profile mode
|
||||
self.metrics = AgentMetrics()
|
||||
|
||||
# Tool definitions in OpenAI format
|
||||
self.tool_definitions = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Read the contents of a file, optionally specifying a line range",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_path": {
|
||||
"type": "string",
|
||||
"description": "Path to the file relative to root directory"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"description": "Line number to start reading from (0-based, default: 0)",
|
||||
"default": 0
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"description": "Number of lines to read (default: read all lines)",
|
||||
"default": None
|
||||
}
|
||||
},
|
||||
"required": ["file_path"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "find",
|
||||
"description": "Find files matching a pattern",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "File name pattern (supports wildcards like *.py)"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": "Directory to search in (default: current directory)",
|
||||
"default": "."
|
||||
}
|
||||
},
|
||||
"required": ["pattern"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "grep",
|
||||
"description": "Search for a pattern in files",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "Regular expression pattern to search for"
|
||||
},
|
||||
"file_path": {
|
||||
"type": "string",
|
||||
"description": "Single file to search in (optional)"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": "Directory to search in (optional)"
|
||||
}
|
||||
},
|
||||
"required": ["pattern"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
logger.info(f"Agent initialized with mode: {mode.value}, model: {model}")
|
||||
|
||||
def _get_system_prompt(self) -> str:
|
||||
"""Get system prompt based on mode"""
|
||||
base_prompt = """You are a helpful AI assistant with access to file system tools.
|
||||
You can read files, find files by pattern, and search for text within files.
|
||||
Use the ReAct pattern: Reason about what to do, then Act using tools, and Observe the results.
|
||||
|
||||
When asked to analyze or summarize code projects, be thorough:
|
||||
1. First use 'find' to discover the structure
|
||||
2. Then read key files to understand the content
|
||||
3. Use 'grep' to search for specific patterns if needed
|
||||
4. Once you have gathered sufficient information, provide your response
|
||||
|
||||
Always think step by step and use tools to gather information. When you have enough information to answer the user's question, simply provide your response without calling any tools."""
|
||||
|
||||
if self.mode == KVCacheMode.DYNAMIC_SYSTEM:
|
||||
# Add timestamp to system prompt (breaks KV cache)
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
return f"{base_prompt}\n\nCURRENT TIME: {timestamp}"
|
||||
|
||||
return base_prompt
|
||||
|
||||
def _get_tools(self) -> List[Dict]:
|
||||
"""Get tool definitions based on mode"""
|
||||
tools = self.tool_definitions.copy()
|
||||
|
||||
if self.mode == KVCacheMode.SHUFFLED_TOOLS:
|
||||
# Shuffle tool order (breaks KV cache)
|
||||
random.shuffle(tools)
|
||||
|
||||
return tools
|
||||
|
||||
def _get_user_profile_message(self) -> Optional[Dict]:
|
||||
"""Get user profile message for dynamic profile mode"""
|
||||
if self.mode == KVCacheMode.DYNAMIC_PROFILE:
|
||||
self.user_credits -= 1
|
||||
return {
|
||||
"role": "user",
|
||||
"content": f"[User Profile: Premium user with {self.user_credits} credits remaining]"
|
||||
}
|
||||
return None
|
||||
|
||||
def _format_messages(self, task: str) -> List[Dict]:
|
||||
"""Format messages based on mode - recreated each iteration for incorrect modes"""
|
||||
messages = []
|
||||
|
||||
# Add system prompt (changes each time for DYNAMIC_SYSTEM mode)
|
||||
messages.append({
|
||||
"role": "system",
|
||||
"content": self._get_system_prompt()
|
||||
})
|
||||
|
||||
# Add user profile if in dynamic profile mode (changes each time)
|
||||
profile_msg = self._get_user_profile_message()
|
||||
if profile_msg:
|
||||
messages.append(profile_msg)
|
||||
|
||||
if self.mode == KVCacheMode.SLIDING_WINDOW:
|
||||
# Keep only the most recent 6 history messages (the window).
|
||||
# conversation_history holds assistant/tool messages, so the raw
|
||||
# slice could start with a tool message whose paired assistant
|
||||
# tool_calls message was trimmed away — the API rejects such a
|
||||
# history. Walk the window start back to the owning assistant
|
||||
# message so every tool message keeps its pair.
|
||||
if self.conversation_history:
|
||||
start = max(0, len(self.conversation_history) - 6)
|
||||
while start > 0 and self.conversation_history[start].get("role") == "tool":
|
||||
start -= 1
|
||||
messages.extend(self.conversation_history[start:])
|
||||
elif self.mode == KVCacheMode.TEXT_FORMAT:
|
||||
# Format all history as plain text (breaks KV cache)
|
||||
# Reformatting each time breaks structured format
|
||||
if self.conversation_history:
|
||||
history_text = "Previous conversation:\n"
|
||||
for msg in self.conversation_history:
|
||||
role = msg['role'].upper()
|
||||
|
||||
# Handle different message types
|
||||
if role == "ASSISTANT":
|
||||
# Also include any content
|
||||
if msg.get('content'):
|
||||
history_text += f"{role}: {msg['content']}\n"
|
||||
# Check for tool calls
|
||||
if msg.get('tool_calls'):
|
||||
history_text += f"{role}: [Making tool calls]\n"
|
||||
for tool_call in msg['tool_calls']:
|
||||
func_name = tool_call.get('function', {}).get('name', 'unknown')
|
||||
func_args = tool_call.get('function', {}).get('arguments', '{}')
|
||||
history_text += f" - Calling {func_name} with args: {func_args}\n"
|
||||
elif role == "TOOL":
|
||||
# Format tool responses
|
||||
tool_content = msg.get('content', '')
|
||||
history_text += f"TOOL RESPONSE: {tool_content}\n"
|
||||
else:
|
||||
# USER, SYSTEM, or other roles
|
||||
content = msg.get('content', '')
|
||||
if content:
|
||||
history_text += f"{role}: {content}\n"
|
||||
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": history_text
|
||||
})
|
||||
else:
|
||||
# For CORRECT, DYNAMIC_SYSTEM, SHUFFLED_TOOLS, DYNAMIC_PROFILE modes
|
||||
# Include full conversation history
|
||||
messages.extend(self.conversation_history)
|
||||
|
||||
# Add current task (always at the end)
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": task
|
||||
})
|
||||
|
||||
return messages
|
||||
|
||||
def _execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
|
||||
"""Execute a tool and return the result"""
|
||||
tool_map = {
|
||||
"read_file": self.tools.read_file,
|
||||
"find": self.tools.find,
|
||||
"grep": self.tools.grep
|
||||
}
|
||||
|
||||
if tool_name not in tool_map:
|
||||
return {"error": f"Unknown tool: {tool_name}", "success": False}
|
||||
|
||||
try:
|
||||
# Filter out any unexpected arguments
|
||||
tool_func = tool_map[tool_name]
|
||||
# Get the expected arguments for this tool
|
||||
import inspect
|
||||
sig = inspect.signature(tool_func)
|
||||
valid_args = {}
|
||||
for param_name in sig.parameters:
|
||||
if param_name in arguments:
|
||||
valid_args[param_name] = arguments[param_name]
|
||||
|
||||
# Log if any arguments were filtered
|
||||
filtered = set(arguments.keys()) - set(valid_args.keys())
|
||||
if filtered and self.verbose:
|
||||
logger.warning(f"Filtered unexpected arguments for {tool_name}: {filtered}")
|
||||
|
||||
return tool_func(**valid_args)
|
||||
except Exception as e:
|
||||
# Return error as tool result instead of raising
|
||||
error_msg = f"Tool execution error: {str(e)}"
|
||||
logger.error(f"{tool_name} failed: {error_msg}")
|
||||
return {"error": error_msg, "success": False}
|
||||
|
||||
|
||||
def execute_task(self, task: str, max_iterations: int = 50) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute a task using ReAct pattern with standard OpenAI tool calling
|
||||
|
||||
Args:
|
||||
task: The task to execute
|
||||
max_iterations: Maximum number of iterations
|
||||
|
||||
Returns:
|
||||
Task execution result with metrics
|
||||
"""
|
||||
start_time = time.time()
|
||||
iteration = 0
|
||||
final_answer = None
|
||||
tool_calls = []
|
||||
|
||||
# Store the original task
|
||||
original_task = task
|
||||
|
||||
while iteration < max_iterations:
|
||||
iteration += 1
|
||||
|
||||
# CRITICAL: Message handling for KV cache demonstration
|
||||
#
|
||||
# CORRECT mode: Build messages once on first iteration, then keep appending
|
||||
# - Maintains stable context → KV cache works efficiently
|
||||
#
|
||||
# INCORRECT modes: Recreate entire messages list from history each iteration
|
||||
# - Forces complete context reconstruction → KV cache invalidated
|
||||
# - Within an iteration, we still append to messages for proper API flow
|
||||
# - But at the start of each new iteration, we rebuild from scratch
|
||||
|
||||
if self.mode == KVCacheMode.CORRECT:
|
||||
# Correct mode: Build messages once, then keep using same list
|
||||
if iteration == 1:
|
||||
messages = self._format_messages(original_task)
|
||||
else:
|
||||
# Incorrect modes: Recreate messages from history each iteration
|
||||
# This forces cache invalidation due to context changes
|
||||
messages = self._format_messages(original_task)
|
||||
|
||||
# Prepare request
|
||||
request_data = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"temperature": _reasoning_safe_temperature(self.model, 0.7),
|
||||
"max_tokens": _reasoning_safe_max_tokens(self.model, 2000)
|
||||
}
|
||||
|
||||
# Add tools for all modes (TEXT_FORMAT still needs tools to work)
|
||||
# TEXT_FORMAT only affects how conversation history is formatted, not tool availability
|
||||
request_data["tools"] = self._get_tools()
|
||||
request_data["tool_choice"] = "auto"
|
||||
|
||||
# Make API call
|
||||
api_start = time.time()
|
||||
try:
|
||||
response = self.client.chat.completions.create(**request_data)
|
||||
|
||||
# Record TTFT for this iteration
|
||||
iteration_ttft = time.time() - api_start
|
||||
self.metrics.ttft_per_iteration.append(iteration_ttft)
|
||||
|
||||
# Record first iteration TTFT separately for backwards compatibility
|
||||
if iteration == 1:
|
||||
self.metrics.ttft = iteration_ttft
|
||||
|
||||
# Extract response
|
||||
message = response.choices[0].message
|
||||
|
||||
# Print assistant content to console (always show, not just verbose)
|
||||
if message.content:
|
||||
print(f"\n🤖 Assistant (Iteration {iteration}):")
|
||||
print("-" * 40)
|
||||
print(message.content)
|
||||
print("-" * 40)
|
||||
|
||||
# Log token usage and cache information
|
||||
if hasattr(response, 'usage'):
|
||||
usage = response.usage
|
||||
self.metrics.prompt_tokens += usage.prompt_tokens
|
||||
self.metrics.completion_tokens += usage.completion_tokens
|
||||
|
||||
# Check for cached tokens (Kimi specific)
|
||||
# The cached_tokens field appears directly in the usage object
|
||||
cached = 0
|
||||
if hasattr(usage, 'cached_tokens'):
|
||||
# Direct attribute on usage object
|
||||
cached = usage.cached_tokens if usage.cached_tokens is not None else 0
|
||||
self.metrics.cached_tokens += cached
|
||||
if cached > 0:
|
||||
self.metrics.cache_hits += 1
|
||||
else:
|
||||
self.metrics.cache_misses += 1
|
||||
else:
|
||||
# Try alternative locations
|
||||
if hasattr(usage, 'prompt_tokens_details'):
|
||||
details = usage.prompt_tokens_details
|
||||
if details and hasattr(details, 'cached_tokens'):
|
||||
cached = details.cached_tokens if details.cached_tokens is not None else 0
|
||||
self.metrics.cached_tokens += cached
|
||||
if cached > 0:
|
||||
self.metrics.cache_hits += 1
|
||||
else:
|
||||
self.metrics.cache_misses += 1
|
||||
|
||||
# Debug logging when verbose and no cached tokens field found
|
||||
if self.verbose and iteration > 1 and cached == 0:
|
||||
logger.debug(f"Usage object attributes: {dir(usage)}")
|
||||
logger.debug(f"Usage data: {usage}")
|
||||
|
||||
if self.verbose:
|
||||
# Log with TTFT for this iteration
|
||||
cache_info = f", cached={cached}" if cached > 0 else ""
|
||||
logger.info(f"Iteration {iteration} - TTFT: {iteration_ttft:.3f}s, "
|
||||
f"Tokens: prompt={usage.prompt_tokens}, "
|
||||
f"completion={usage.completion_tokens}"
|
||||
f"{cache_info}")
|
||||
|
||||
# Handle tool calls using standard OpenAI format
|
||||
if hasattr(message, 'tool_calls') and message.tool_calls:
|
||||
# Add the assistant message with tool calls
|
||||
# Always append to messages for current iteration
|
||||
messages.append(message.model_dump())
|
||||
# Also append to history for next iteration
|
||||
self.conversation_history.append(message.model_dump())
|
||||
|
||||
for tool_call in message.tool_calls:
|
||||
function_name = tool_call.function.name
|
||||
|
||||
# Parse arguments safely
|
||||
try:
|
||||
function_args = json.loads(tool_call.function.arguments)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse tool arguments: {e}")
|
||||
function_args = {}
|
||||
result = {"error": f"Invalid tool arguments: {str(e)}", "success": False}
|
||||
else:
|
||||
if self.verbose:
|
||||
logger.info(f"Executing tool: {function_name} with args: {function_args}")
|
||||
|
||||
# Execute tool (errors are handled internally and returned as results)
|
||||
result = self._execute_tool(function_name, function_args)
|
||||
|
||||
# Record tool call
|
||||
tc = ToolCall(name=function_name, arguments=function_args, result=result)
|
||||
tool_calls.append(tc)
|
||||
|
||||
# Print tool result summary
|
||||
if result.get("success"):
|
||||
# Success - show brief summary
|
||||
if function_name == "read_file":
|
||||
lines_info = f"{result.get('lines_read', 'unknown')} lines"
|
||||
if result.get('offset', 0) > 0 or result.get('size'):
|
||||
lines_info += f" (lines {result.get('offset', 0)}-{result.get('end_line', '?')})"
|
||||
print(f" ✓ {function_name}: Read {lines_info}")
|
||||
elif function_name == "find":
|
||||
print(f" ✓ {function_name}: Found {result.get('count', 0)} files")
|
||||
elif function_name == "grep":
|
||||
print(f" ✓ {function_name}: Found {result.get('match_count', 0)} matches")
|
||||
else:
|
||||
print(f" ✓ {function_name}: Success")
|
||||
else:
|
||||
# Error - show the error message
|
||||
print(f" ✗ {function_name}: {result.get('error', 'Unknown error')}")
|
||||
|
||||
# Add tool result as proper tool message (including errors)
|
||||
tool_message = {
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": json.dumps(result)
|
||||
}
|
||||
# Always append to messages for current iteration
|
||||
messages.append(tool_message)
|
||||
# Also append to history for next iteration
|
||||
self.conversation_history.append(tool_message)
|
||||
|
||||
# Log if tool returned an error
|
||||
if not result.get("success", True):
|
||||
if self.verbose:
|
||||
logger.warning(f"Tool {function_name} returned error: {result.get('error', 'Unknown error')}")
|
||||
|
||||
elif message.content:
|
||||
# No tool calls - consider this the final answer
|
||||
final_answer = message.content
|
||||
# Always append to messages for current iteration
|
||||
messages.append(message.model_dump())
|
||||
# Also append to history for next iteration
|
||||
self.conversation_history.append(message.model_dump())
|
||||
if self.verbose:
|
||||
logger.info("No tool calls in response - considering as final answer")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in iteration {iteration}: {str(e)}")
|
||||
break
|
||||
|
||||
# Calculate final metrics
|
||||
self.metrics.total_time = time.time() - start_time
|
||||
self.metrics.iterations = iteration
|
||||
self.metrics.tool_calls = len(tool_calls)
|
||||
|
||||
return {
|
||||
"success": final_answer is not None,
|
||||
"final_answer": final_answer,
|
||||
"iterations": iteration,
|
||||
"tool_calls": tool_calls,
|
||||
"metrics": self.metrics,
|
||||
"mode": self.mode.value
|
||||
}
|
||||
|
||||
|
||||
def compare_implementations(api_key: str, task: str, root_dir: str = ".",
|
||||
model: str = "kimi-k2.6") -> Dict[str, Any]:
|
||||
"""
|
||||
Compare different KV cache implementations
|
||||
|
||||
Args:
|
||||
api_key: API key for Kimi
|
||||
task: Task to execute
|
||||
root_dir: Root directory for file operations
|
||||
model: Model to use for all modes
|
||||
|
||||
Returns:
|
||||
Comparison results
|
||||
"""
|
||||
results = {}
|
||||
|
||||
for mode in KVCacheMode:
|
||||
logger.info(f"\n{'='*60}")
|
||||
logger.info(f"Testing mode: {mode.value}")
|
||||
logger.info(f"{'='*60}")
|
||||
|
||||
agent = KVCacheAgent(api_key=api_key, mode=mode, model=model, root_dir=root_dir, verbose=True)
|
||||
result = agent.execute_task(task)
|
||||
|
||||
results[mode.value] = {
|
||||
"success": result["success"],
|
||||
"iterations": result["iterations"],
|
||||
"tool_calls": result["tool_calls"],
|
||||
"metrics": asdict(result["metrics"])
|
||||
}
|
||||
|
||||
# Log summary
|
||||
metrics = result["metrics"]
|
||||
logger.info(f"\nMode: {mode.value}")
|
||||
logger.info(f"First TTFT: {metrics.ttft:.3f}s")
|
||||
|
||||
# Log TTFT progression
|
||||
if metrics.ttft_per_iteration:
|
||||
ttft_summary = ", ".join([f"{t:.3f}s" for t in metrics.ttft_per_iteration[:5]])
|
||||
if len(metrics.ttft_per_iteration) > 5:
|
||||
ttft_summary += f"... ({len(metrics.ttft_per_iteration)} total)"
|
||||
logger.info(f"TTFT per iteration: [{ttft_summary}]")
|
||||
|
||||
# Calculate TTFT improvement from first to last
|
||||
if len(metrics.ttft_per_iteration) > 1:
|
||||
improvement = (metrics.ttft_per_iteration[0] - metrics.ttft_per_iteration[-1]) / metrics.ttft_per_iteration[0] * 100
|
||||
logger.info(f"TTFT improvement: {improvement:.1f}% (first vs last)")
|
||||
|
||||
logger.info(f"Total Time: {metrics.total_time:.3f}s")
|
||||
logger.info(f"Cached Tokens: {metrics.cached_tokens}")
|
||||
logger.info(f"Cache Hits: {metrics.cache_hits}")
|
||||
logger.info(f"Cache Misses: {metrics.cache_misses}")
|
||||
logger.info(f"Total Tokens: {metrics.prompt_tokens + metrics.completion_tokens}")
|
||||
|
||||
return results
|
||||
@@ -0,0 +1,6 @@
|
||||
# Moonshot / Kimi API 配置(本实验主用 Moonshot 官方接口)
|
||||
MOONSHOT_API_KEY=your_moonshot_api_key_here
|
||||
|
||||
# 通用回退:未设置 MOONSHOT_API_KEY / KIMI_API_KEY 时,若配置了 OPENROUTER_API_KEY,
|
||||
# 则自动改走 OpenRouter(kimi-* 模型名会映射为 moonshotai/kimi-k2)。
|
||||
# OPENROUTER_API_KEY=your-openrouter-api-key
|
||||
@@ -0,0 +1,548 @@
|
||||
"""
|
||||
Main script to demonstrate KV cache importance
|
||||
Runs the ReAct agent with different implementations and compares performance
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import glob
|
||||
import json
|
||||
import argparse
|
||||
import logging
|
||||
from typing import Dict, List, Any
|
||||
from datetime import datetime
|
||||
from dataclasses import asdict
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from agent import KVCacheAgent, KVCacheMode, AgentMetrics, compare_implementations
|
||||
|
||||
# Default model (Moonshot / Kimi). The whole current Kimi family (k2.5/k2.6/
|
||||
# k2.7/k3) reports cached_tokens for automatic prefix caching AND reasons, so it
|
||||
# only accepts temperature=1 (agent.py handles that automatically). kimi-k2.6 has
|
||||
# the lightest reasoning footprint of the cache-reporting models, giving the
|
||||
# cleanest TTFT while still exposing the prefix-cache hit metric this demo needs.
|
||||
# (The non-reasoning moonshot-v1-* models do NOT report cached_tokens, so they
|
||||
# cannot demonstrate the cache effect.)
|
||||
DEFAULT_MODEL = "kimi-k2.6"
|
||||
DEFAULT_ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('kv_cache_demo.log'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Metrics helpers (shared by live comparison and offline report)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _coerce_metrics(metrics: Any) -> Dict[str, Any]:
|
||||
"""Normalize a stored metrics value into a plain dict.
|
||||
|
||||
Handles both formats found in result files:
|
||||
- dict: produced by --compare (asdict) and by the fixed --mode path
|
||||
- str : legacy single-mode files that stored repr(AgentMetrics(...))
|
||||
because json.dump used default=str
|
||||
"""
|
||||
if isinstance(metrics, dict):
|
||||
return metrics
|
||||
if isinstance(metrics, str) and metrics.startswith("AgentMetrics("):
|
||||
# Safe eval: only AgentMetrics is exposed, no builtins.
|
||||
try:
|
||||
obj = eval(metrics, {"__builtins__": {}}, {"AgentMetrics": AgentMetrics})
|
||||
return asdict(obj)
|
||||
except Exception as e: # pragma: no cover - defensive
|
||||
logger.warning(f"Could not parse legacy metrics string: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
def _avg_ttft(m: Dict[str, Any]) -> float:
|
||||
"""Average TTFT across iterations, falling back to first-iteration TTFT."""
|
||||
lst = m.get("ttft_per_iteration") or []
|
||||
return sum(lst) / len(lst) if lst else float(m.get("ttft", 0.0) or 0.0)
|
||||
|
||||
|
||||
def _hit_rate(m: Dict[str, Any]) -> float:
|
||||
total = (m.get("cache_hits", 0) or 0) + (m.get("cache_misses", 0) or 0)
|
||||
return (m.get("cache_hits", 0) or 0) / total * 100 if total else 0.0
|
||||
|
||||
|
||||
def _billable_tokens(m: Dict[str, Any], cache_price_ratio: float) -> float:
|
||||
"""Illustrative billable prompt tokens under a prompt-cache discount.
|
||||
|
||||
cached tokens are charged at cache_price_ratio of the normal price; the
|
||||
rest at full price. This is a transparent function of the *measured*
|
||||
token counts and a user-supplied ratio - it is not a fabricated
|
||||
provider-specific price.
|
||||
"""
|
||||
prompt = m.get("prompt_tokens", 0) or 0
|
||||
cached = m.get("cached_tokens", 0) or 0
|
||||
cached = min(cached, prompt)
|
||||
return (prompt - cached) + cached * cache_price_ratio
|
||||
|
||||
|
||||
def print_comparison_table(results: Dict[str, Any], cache_price_ratio: float = 0.1) -> None:
|
||||
"""Render the cross-strategy comparison table (latency / cache / cost)."""
|
||||
print(f"\n{'Mode':<16} {'Iters':<6} {'1st TTFT':<10} {'Avg TTFT':<10} "
|
||||
f"{'Total(s)':<10} {'Prompt':<9} {'Cached':<9} {'Hit%':<7} "
|
||||
f"{'Cache%':<8} {'Bill.Tok':<10} {'Save%':<7}")
|
||||
print("-" * 112)
|
||||
|
||||
for mode, data in results.items():
|
||||
m = _coerce_metrics(data.get("metrics", {}))
|
||||
prompt = m.get("prompt_tokens", 0) or 0
|
||||
cached = m.get("cached_tokens", 0) or 0
|
||||
iters = data.get("iterations", m.get("iterations", 0)) or 0
|
||||
cache_pct = cached / prompt * 100 if prompt else 0.0
|
||||
billable = _billable_tokens(m, cache_price_ratio)
|
||||
save_pct = (prompt - billable) / prompt * 100 if prompt else 0.0
|
||||
|
||||
print(f"{mode:<16} {iters:<6} {float(m.get('ttft', 0.0) or 0.0):<10.3f} "
|
||||
f"{_avg_ttft(m):<10.3f} {float(m.get('total_time', 0.0) or 0.0):<10.3f} "
|
||||
f"{prompt:<9,} {cached:<9,} {_hit_rate(m):<7.1f} "
|
||||
f"{cache_pct:<8.1f} {billable:<10,.0f} {save_pct:<7.1f}")
|
||||
|
||||
print("-" * 112)
|
||||
print(f"注:Bill.Tok / Save% 假设缓存 token 按正常价的 {cache_price_ratio:.0%} 计费"
|
||||
f"(可用 --cache-price-ratio 调整),仅为成本示意,非某家服务商实际报价。")
|
||||
|
||||
|
||||
def load_result_files(paths: List[str]) -> Dict[str, Any]:
|
||||
"""Load result_*.json files into a {mode: {...}} dict for offline reporting."""
|
||||
results: Dict[str, Any] = {}
|
||||
for path in sorted(paths):
|
||||
try:
|
||||
with open(path, 'r') as f:
|
||||
data = json.load(f)
|
||||
except Exception as e:
|
||||
logger.warning(f"Skipping {path}: {e}")
|
||||
continue
|
||||
|
||||
# A comparison_*.json holds many modes; a result_*.json holds one.
|
||||
if "mode" not in data and all(isinstance(v, dict) and "metrics" in v
|
||||
for v in data.values()):
|
||||
for mode, entry in data.items():
|
||||
results[mode] = {"metrics": _coerce_metrics(entry.get("metrics", {})),
|
||||
"iterations": entry.get("iterations"),
|
||||
"_source": path}
|
||||
else:
|
||||
mode = data.get("mode", os.path.splitext(os.path.basename(path))[0])
|
||||
results[mode] = {"metrics": _coerce_metrics(data.get("metrics", {})),
|
||||
"iterations": data.get("iterations"),
|
||||
"_source": path}
|
||||
return results
|
||||
|
||||
|
||||
def run_report(inputs: List[str] = None, cache_price_ratio: float = 0.1) -> None:
|
||||
"""Offline: build the comparison table from existing result_*.json files.
|
||||
|
||||
No API key required - reads previously saved runs so the final result is
|
||||
legible in one command without re-hitting the model.
|
||||
"""
|
||||
if not inputs:
|
||||
inputs = ["result_*.json", "comparison_*.json"]
|
||||
|
||||
paths: List[str] = []
|
||||
for item in inputs:
|
||||
if os.path.isdir(item):
|
||||
paths.extend(glob.glob(os.path.join(item, "result_*.json")))
|
||||
paths.extend(glob.glob(os.path.join(item, "comparison_*.json")))
|
||||
else:
|
||||
paths.extend(glob.glob(item))
|
||||
|
||||
paths = sorted(set(paths))
|
||||
if not paths:
|
||||
logger.error("未找到任何 result_*.json / comparison_*.json 结果文件。"
|
||||
"请先运行 --mode 或 --compare 生成结果,或用 --input 指定路径。")
|
||||
sys.exit(1)
|
||||
|
||||
results = load_result_files(paths)
|
||||
|
||||
print("\n" + "=" * 112)
|
||||
print("KV CACHE 离线对比报告(基于已保存的实测结果)")
|
||||
print("=" * 112)
|
||||
print(f"数据来源({len(paths)} 个文件):")
|
||||
for mode, data in results.items():
|
||||
print(f" • {mode:<16} ← {os.path.basename(data.get('_source', '?'))}")
|
||||
|
||||
print_comparison_table(results, cache_price_ratio)
|
||||
|
||||
print("\n📝 说明:不同结果文件可能来自不同任务/时间,绝对数值仅供同一次运行内横向对比;"
|
||||
"如需严格对照,请用 --compare 在同一任务下一次性生成全部模式的数据。")
|
||||
|
||||
|
||||
def create_summary_task() -> str:
|
||||
"""Create a task that requires reading multiple files"""
|
||||
return """Please analyze and summarize all the projects in the chapter1 and chapter2 directories.
|
||||
For each project:
|
||||
1. Find all Python files
|
||||
2. Read the main files and understand the functionality
|
||||
3. Identify the key features and purpose
|
||||
4. Provide a comprehensive summary
|
||||
|
||||
Start with chapter1 projects, then move to chapter2. Be thorough in your analysis."""
|
||||
|
||||
|
||||
def run_single_mode(api_key: str, mode: str, task: str = None, root_dir: str = DEFAULT_ROOT_DIR,
|
||||
model: str = DEFAULT_MODEL, output: str = None):
|
||||
"""
|
||||
Run agent in a single mode
|
||||
|
||||
Args:
|
||||
api_key: API key for Kimi
|
||||
mode: KV cache mode to use
|
||||
task: Custom task (optional)
|
||||
root_dir: Root directory for file operations (default: "../.." = repository root)
|
||||
model: Model to use
|
||||
output: Output path for the result JSON (optional; auto-named if omitted)
|
||||
"""
|
||||
# Parse mode
|
||||
mode_map = {
|
||||
"correct": KVCacheMode.CORRECT,
|
||||
"dynamic_system": KVCacheMode.DYNAMIC_SYSTEM,
|
||||
"shuffled_tools": KVCacheMode.SHUFFLED_TOOLS,
|
||||
"dynamic_profile": KVCacheMode.DYNAMIC_PROFILE,
|
||||
"sliding_window": KVCacheMode.SLIDING_WINDOW,
|
||||
"text_format": KVCacheMode.TEXT_FORMAT
|
||||
}
|
||||
|
||||
if mode not in mode_map:
|
||||
logger.error(f"Invalid mode: {mode}")
|
||||
logger.info(f"Valid modes: {', '.join(mode_map.keys())}")
|
||||
return
|
||||
|
||||
# Use default task if not provided
|
||||
if not task:
|
||||
task = create_summary_task()
|
||||
|
||||
logger.info(f"Running in mode: {mode}")
|
||||
logger.info(f"Task: {task}")
|
||||
logger.info("="*80)
|
||||
|
||||
# Create agent and execute task
|
||||
agent = KVCacheAgent(
|
||||
api_key=api_key,
|
||||
mode=mode_map[mode],
|
||||
model=model,
|
||||
root_dir=root_dir,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = agent.execute_task(task, max_iterations=30)
|
||||
|
||||
# Print results
|
||||
print("\n" + "="*80)
|
||||
print(f"EXECUTION RESULTS - Mode: {mode}")
|
||||
print("="*80)
|
||||
|
||||
metrics = result["metrics"]
|
||||
print(f"\n📊 Performance Metrics:")
|
||||
print(f" • Time to First Token (TTFT): {metrics.ttft:.3f} seconds")
|
||||
|
||||
# Show TTFT progression
|
||||
if metrics.ttft_per_iteration:
|
||||
print(f" • TTFT per iteration:")
|
||||
for i, ttft in enumerate(metrics.ttft_per_iteration, 1):
|
||||
print(f" Iteration {i}: {ttft:.3f}s")
|
||||
|
||||
# Show improvement
|
||||
if len(metrics.ttft_per_iteration) > 1:
|
||||
first_ttft = metrics.ttft_per_iteration[0]
|
||||
last_ttft = metrics.ttft_per_iteration[-1]
|
||||
avg_after_first = sum(metrics.ttft_per_iteration[1:]) / len(metrics.ttft_per_iteration[1:])
|
||||
print(f" • TTFT Analysis:")
|
||||
print(f" First iteration: {first_ttft:.3f}s")
|
||||
print(f" Last iteration: {last_ttft:.3f}s")
|
||||
print(f" Average (after first): {avg_after_first:.3f}s")
|
||||
improvement = (first_ttft - last_ttft) / first_ttft * 100
|
||||
print(f" Improvement: {improvement:.1f}%")
|
||||
|
||||
print(f" • Total Execution Time: {metrics.total_time:.3f} seconds")
|
||||
print(f" • Iterations: {result['iterations']}")
|
||||
print(f" • Tool Calls: {len(result['tool_calls'])}")
|
||||
|
||||
print(f"\n🔄 Cache Statistics:")
|
||||
print(f" • Cached Tokens: {metrics.cached_tokens:,}")
|
||||
print(f" • Cache Hits: {metrics.cache_hits}")
|
||||
print(f" • Cache Misses: {metrics.cache_misses}")
|
||||
if metrics.cache_hits + metrics.cache_misses > 0:
|
||||
hit_rate = metrics.cache_hits / (metrics.cache_hits + metrics.cache_misses) * 100
|
||||
print(f" • Cache Hit Rate: {hit_rate:.1f}%")
|
||||
|
||||
print(f"\n💰 Token Usage:")
|
||||
print(f" • Prompt Tokens: {metrics.prompt_tokens:,}")
|
||||
print(f" • Completion Tokens: {metrics.completion_tokens:,}")
|
||||
print(f" • Total Tokens: {metrics.prompt_tokens + metrics.completion_tokens:,}")
|
||||
if metrics.prompt_tokens > 0:
|
||||
cache_ratio = metrics.cached_tokens / metrics.prompt_tokens * 100
|
||||
print(f" • Cache Ratio: {cache_ratio:.1f}% of prompt tokens cached")
|
||||
|
||||
# Show tool calls summary
|
||||
if result["tool_calls"]:
|
||||
print(f"\n🔧 Tool Calls Summary:")
|
||||
tool_counts = {}
|
||||
for tc in result["tool_calls"]:
|
||||
tool_counts[tc.name] = tool_counts.get(tc.name, 0) + 1
|
||||
for tool_name, count in tool_counts.items():
|
||||
print(f" • {tool_name}: {count} calls")
|
||||
|
||||
# Save detailed results
|
||||
output_file = output or f"result_{mode}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
with open(output_file, 'w') as f:
|
||||
# Convert to serializable format. Store metrics as a dict (via asdict)
|
||||
# so the file can be re-loaded later by --report; tool calls likewise.
|
||||
result_copy = result.copy()
|
||||
result_copy["metrics"] = asdict(result["metrics"])
|
||||
result_copy["tool_calls"] = [
|
||||
{
|
||||
"name": tc.name,
|
||||
"arguments": tc.arguments,
|
||||
"timestamp": tc.timestamp
|
||||
}
|
||||
for tc in result["tool_calls"]
|
||||
]
|
||||
json.dump(result_copy, f, indent=2, default=str)
|
||||
|
||||
print(f"\n💾 Detailed results saved to: {output_file}")
|
||||
|
||||
|
||||
def select_mode_interactive():
|
||||
"""
|
||||
Interactive mode selection menu
|
||||
|
||||
Returns:
|
||||
Selected mode string or None for all modes
|
||||
"""
|
||||
modes = [
|
||||
("correct", "✅ Correct Implementation - Optimal KV cache usage"),
|
||||
("dynamic_system", "❌ Dynamic System Prompt - Adds timestamps"),
|
||||
("shuffled_tools", "❌ Shuffled Tools - Randomizes tool order"),
|
||||
("dynamic_profile", "❌ Dynamic Profile - Updates user credits"),
|
||||
("sliding_window", "❌ Sliding Window - Keeps only recent messages"),
|
||||
("text_format", "❌ Text Format - Plain text instead of structured"),
|
||||
("compare", "📊 Compare All - Run all modes and compare"),
|
||||
]
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("KV CACHE DEMONSTRATION - MODE SELECTION")
|
||||
print("="*60)
|
||||
print("\nSelect a mode to run:\n")
|
||||
|
||||
for i, (mode, description) in enumerate(modes, 1):
|
||||
print(f" {i}. {description}")
|
||||
|
||||
print("\n 0. Exit")
|
||||
print("-"*60)
|
||||
|
||||
while True:
|
||||
try:
|
||||
choice = input("\nEnter your choice (0-7): ").strip()
|
||||
choice_num = int(choice)
|
||||
|
||||
if choice_num == 0:
|
||||
print("Exiting...")
|
||||
sys.exit(0)
|
||||
elif 1 <= choice_num <= 6:
|
||||
selected = modes[choice_num - 1][0]
|
||||
print(f"\n✓ Selected: {modes[choice_num - 1][1]}")
|
||||
return selected
|
||||
elif choice_num == 7:
|
||||
print("\n✓ Selected: Compare all modes")
|
||||
return "compare"
|
||||
else:
|
||||
print("Invalid choice. Please enter a number between 0 and 7.")
|
||||
except ValueError:
|
||||
print("Invalid input. Please enter a number.")
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nExiting...")
|
||||
sys.exit(0)
|
||||
|
||||
def run_comparison(api_key: str, task: str = None, root_dir: str = DEFAULT_ROOT_DIR,
|
||||
model: str = DEFAULT_MODEL, output: str = None,
|
||||
cache_price_ratio: float = 0.1):
|
||||
"""
|
||||
Run comparison across all modes
|
||||
|
||||
Args:
|
||||
api_key: API key for Kimi
|
||||
task: Custom task (optional)
|
||||
root_dir: Root directory for file operations (default: "../.." = repository root)
|
||||
model: Model to use for all modes
|
||||
output: Output path for the comparison JSON (optional; auto-named if omitted)
|
||||
cache_price_ratio: Assumed price of a cached token vs a normal token (cost column)
|
||||
"""
|
||||
# Use default task if not provided
|
||||
if not task:
|
||||
task = create_summary_task()
|
||||
|
||||
logger.info("Starting KV Cache Comparison Study")
|
||||
logger.info(f"Task: {task[:200]}...")
|
||||
logger.info("="*80)
|
||||
|
||||
# Run comparison
|
||||
results = compare_implementations(api_key, task, root_dir, model=model)
|
||||
|
||||
# Print comparison table
|
||||
print("\n" + "="*112)
|
||||
print("KV CACHE COMPARISON RESULTS")
|
||||
print("="*112)
|
||||
|
||||
print_comparison_table(results, cache_price_ratio)
|
||||
|
||||
# Analyze results
|
||||
print("\n" + "="*80)
|
||||
print("ANALYSIS")
|
||||
print("="*80)
|
||||
|
||||
# Find best and worst performers
|
||||
correct_metrics = results["correct"]["metrics"]
|
||||
|
||||
print("\n🏆 Performance Impact (compared to correct implementation):")
|
||||
for mode, data in results.items():
|
||||
if mode == "correct":
|
||||
continue
|
||||
|
||||
metrics = data["metrics"]
|
||||
ttft_diff = ((metrics["ttft"] - correct_metrics["ttft"]) / correct_metrics["ttft"]) * 100
|
||||
total_diff = ((metrics["total_time"] - correct_metrics["total_time"]) / correct_metrics["total_time"]) * 100
|
||||
cache_diff = correct_metrics["cached_tokens"] - metrics["cached_tokens"]
|
||||
|
||||
print(f"\n{mode}:")
|
||||
print(f" • TTFT: {'+' if ttft_diff > 0 else ''}{ttft_diff:.1f}% "
|
||||
f"({'slower' if ttft_diff > 0 else 'faster'})")
|
||||
print(f" • Total Time: {'+' if total_diff > 0 else ''}{total_diff:.1f}% "
|
||||
f"({'slower' if total_diff > 0 else 'faster'})")
|
||||
print(f" • Lost Cached Tokens: {cache_diff:,}")
|
||||
|
||||
# Show TTFT progression comparison
|
||||
print("\n📈 TTFT Progression (first 5 iterations):")
|
||||
for mode, data in results.items():
|
||||
metrics = data["metrics"]
|
||||
ttft_list = metrics.get("ttft_per_iteration", [])[:5]
|
||||
if ttft_list:
|
||||
ttft_str = " → ".join([f"{t:.2f}s" for t in ttft_list])
|
||||
print(f" {mode:<20}: {ttft_str}")
|
||||
|
||||
# Key insights
|
||||
print("\n📝 Key Insights:")
|
||||
print(" 1. The correct implementation maintains stable context for optimal KV cache usage")
|
||||
print(" 2. TTFT improves dramatically after first iteration when cache is utilized")
|
||||
print(" 3. Dynamic system prompts invalidate the entire cache on each request")
|
||||
print(" 4. Shuffling tools breaks cache even though the functionality is identical")
|
||||
print(" 5. Dynamic user profiles add unnecessary context changes")
|
||||
print(" 6. Sliding windows may seem to reduce context but actually harm cache efficiency")
|
||||
print(" 7. Text formatting breaks the structured message format that enables caching")
|
||||
|
||||
# Save comparison results
|
||||
output_file = output or f"comparison_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
with open(output_file, 'w') as f:
|
||||
json.dump(results, f, indent=2, default=str)
|
||||
|
||||
print(f"\n💾 Comparison results saved to: {output_file}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="KV Cache 实验:用 ReAct Agent 对比不同上下文构造策略对前缀缓存"
|
||||
"(KV Cache / Prompt Cache)命中率、TTFT 延迟与成本的影响。",
|
||||
epilog="示例:\n"
|
||||
" python main.py --mode correct # 运行单个策略\n"
|
||||
" python main.py --compare # 一次跑完所有策略并打印对比表\n"
|
||||
" python main.py --report # 离线:读取已有 result_*.json 打印对比表(无需 API Key)\n"
|
||||
" python main.py --mode sliding_window --model kimi-k2.6 --output run.json\n"
|
||||
"\n可选策略(--mode):correct, dynamic_system, shuffled_tools,\n"
|
||||
" dynamic_profile, sliding_window, text_format",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("--api-key", type=str,
|
||||
help="Moonshot/Kimi API Key(也可用环境变量 MOONSHOT_API_KEY)")
|
||||
parser.add_argument("--model", type=str, default=DEFAULT_MODEL,
|
||||
help=f"使用的模型名(默认:{DEFAULT_MODEL})")
|
||||
parser.add_argument("--mode", type=str,
|
||||
help="运行单个策略:correct / dynamic_system / shuffled_tools / "
|
||||
"dynamic_profile / sliding_window / text_format")
|
||||
parser.add_argument("--compare", action="store_true",
|
||||
help="依次运行全部策略并打印横向对比表(需要 API Key)")
|
||||
parser.add_argument("--report", action="store_true",
|
||||
help="离线模式:从已保存的 result_*.json / comparison_*.json 生成对比表(无需 API Key)")
|
||||
parser.add_argument("--input", type=str, nargs="*", default=None,
|
||||
help="配合 --report:指定结果文件、通配符或目录(默认:当前目录下的 result_*.json 与 comparison_*.json)")
|
||||
parser.add_argument("--output", type=str,
|
||||
help="结果 JSON 的输出路径(默认按模式和时间戳自动命名)")
|
||||
parser.add_argument("--cache-price-ratio", type=float, default=0.1,
|
||||
help="成本估算中缓存 token 相对正常 token 的计费比例(默认:0.1,即缓存读取按一折计),仅作示意")
|
||||
parser.add_argument("--task", type=str, help="自定义任务描述(默认:分析并总结项目代码)")
|
||||
parser.add_argument("--root-dir", type=str, default=DEFAULT_ROOT_DIR,
|
||||
help="文件工具的根目录(默认:仓库根目录,供 Agent 读取代码)")
|
||||
parser.add_argument("--interactive", action="store_true", default=True,
|
||||
help="交互式菜单选择策略(默认开启)")
|
||||
parser.add_argument("--no-interactive", dest="interactive", action="store_false",
|
||||
help="关闭交互式菜单")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Offline report needs no API key - handle it first.
|
||||
if args.report:
|
||||
run_report(args.input, args.cache_price_ratio)
|
||||
return
|
||||
|
||||
# Get API key. 优先 Moonshot/Kimi 官方 key;缺失时回退到 OPENROUTER_API_KEY
|
||||
# (KVCacheAgent 会据此自动切换到 OpenRouter 端点并映射模型名)。
|
||||
api_key = (args.api_key or os.getenv("MOONSHOT_API_KEY")
|
||||
or os.getenv("KIMI_API_KEY") or os.getenv("OPENROUTER_API_KEY"))
|
||||
if not api_key:
|
||||
logger.error("请通过 --api-key 或环境变量 MOONSHOT_API_KEY / KIMI_API_KEY / "
|
||||
"OPENROUTER_API_KEY 提供 API Key;"
|
||||
"若只想查看已有结果,可使用 --report(无需 API Key)。")
|
||||
sys.exit(1)
|
||||
|
||||
# Run based on mode
|
||||
if args.compare:
|
||||
# Explicit --compare flag overrides interactive mode
|
||||
run_comparison(api_key, args.task, args.root_dir, args.model, args.output,
|
||||
args.cache_price_ratio)
|
||||
elif args.mode:
|
||||
# Explicit --mode flag overrides interactive mode
|
||||
run_single_mode(api_key, args.mode, args.task, args.root_dir, args.model, args.output)
|
||||
elif args.interactive and not args.task:
|
||||
# Interactive mode selection (default)
|
||||
selected_mode = select_mode_interactive()
|
||||
if selected_mode == "compare":
|
||||
run_comparison(api_key, args.task, args.root_dir, args.model, args.output,
|
||||
args.cache_price_ratio)
|
||||
else:
|
||||
run_single_mode(api_key, selected_mode, args.task, args.root_dir, args.model, args.output)
|
||||
else:
|
||||
# If task is provided without mode, ask which mode to use
|
||||
if args.task:
|
||||
print(f"\n📝 Custom task provided: {args.task}")
|
||||
selected_mode = select_mode_interactive()
|
||||
if selected_mode == "compare":
|
||||
run_comparison(api_key, args.task, args.root_dir, args.model, args.output,
|
||||
args.cache_price_ratio)
|
||||
else:
|
||||
run_single_mode(api_key, selected_mode, args.task, args.root_dir, args.model, args.output)
|
||||
else:
|
||||
# Fallback to interactive mode
|
||||
selected_mode = select_mode_interactive()
|
||||
if selected_mode == "compare":
|
||||
run_comparison(api_key, args.task, args.root_dir, args.model, args.output,
|
||||
args.cache_price_ratio)
|
||||
else:
|
||||
run_single_mode(api_key, selected_mode, args.task, args.root_dir, args.model, args.output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
# KV Cache Demonstration Requirements
|
||||
# Core dependencies for ReAct agent with Kimi K3 model
|
||||
|
||||
# Shared provider resolver from the repository root. Run this requirements file
|
||||
# from the experiment directory, as shown in the README.
|
||||
-e ../..
|
||||
|
||||
# OpenAI client for Kimi API compatibility
|
||||
openai>=1.35.0
|
||||
|
||||
# Utilities
|
||||
python-dotenv>=1.0.0 # For environment variable management
|
||||
|
||||
# Optional but recommended for better formatting
|
||||
rich>=13.7.0 # For better console output
|
||||
tabulate>=0.9.0 # For table formatting
|
||||
|
||||
# Development dependencies (optional)
|
||||
# pytest>=7.4.0 # For testing
|
||||
# black>=23.0.0 # For code formatting
|
||||
# pylint>=3.0.0 # For code linting
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
||||
"""Pytest bootstrap for the kv-cache experiment tests."""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import types
|
||||
|
||||
|
||||
EXPERIMENT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(EXPERIMENT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(EXPERIMENT_ROOT))
|
||||
|
||||
try:
|
||||
import openai # noqa: F401
|
||||
except ImportError:
|
||||
openai_stub = types.ModuleType("openai")
|
||||
openai_stub.OpenAI = object
|
||||
sys.modules.setdefault("openai", openai_stub)
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Helpers for running kv-cache manual smoke scripts from tests/manual."""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
def add_project_root() -> Path:
|
||||
project_root = Path(__file__).resolve().parents[2]
|
||||
if str(project_root) not in sys.path:
|
||||
sys.path.insert(0, str(project_root))
|
||||
return project_root
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Manual live check for agent recovery after tool errors."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from _bootstrap import add_project_root
|
||||
|
||||
add_project_root()
|
||||
|
||||
from agent import KVCacheAgent, KVCacheMode
|
||||
|
||||
|
||||
def check_agent_error_recovery():
|
||||
"""Run the live agent against an intentionally failing tool path."""
|
||||
api_key = os.getenv("MOONSHOT_API_KEY")
|
||||
if not api_key:
|
||||
print("❌ Please set MOONSHOT_API_KEY environment variable")
|
||||
sys.exit(1)
|
||||
|
||||
print("🧪 Testing agent error recovery")
|
||||
print("=" * 60)
|
||||
|
||||
agent = KVCacheAgent(
|
||||
api_key=api_key,
|
||||
mode=KVCacheMode.CORRECT,
|
||||
root_dir="../..",
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
task = """Please do the following:
|
||||
1. Try to read a file that doesn't exist: 'non_existent_file.txt'
|
||||
2. Then find Python files in chapter1/context directory
|
||||
3. Tell me what you found"""
|
||||
|
||||
print(f"Task: {task[:100]}...")
|
||||
result = agent.execute_task(task, max_iterations=10)
|
||||
|
||||
print(f"\n✓ Completed in {result['iterations']} iterations")
|
||||
print(f"✓ Tool calls made: {len(result['tool_calls'])}")
|
||||
|
||||
error_count = 0
|
||||
for tool_call in result["tool_calls"]:
|
||||
if tool_call.result and not tool_call.result.get("success", True):
|
||||
error_count += 1
|
||||
print(
|
||||
f"• Tool error in {tool_call.name}: "
|
||||
f"{tool_call.result.get('error', 'Unknown')[:50]}..."
|
||||
)
|
||||
|
||||
print(f"✓ Errors encountered and handled: {error_count}")
|
||||
print(f"✓ Agent continued despite errors: {result['success']}")
|
||||
|
||||
if result["final_answer"]:
|
||||
print("\nFinal answer provided despite errors:")
|
||||
print(f"{result['final_answer'][:200]}...")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_agent_error_recovery()
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify KV cache is properly invalidated in incorrect modes
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from _bootstrap import add_project_root
|
||||
|
||||
add_project_root()
|
||||
|
||||
from agent import KVCacheAgent, KVCacheMode
|
||||
|
||||
# Set up logging to see details
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def test_cache_invalidation():
|
||||
"""Test that incorrect modes properly invalidate KV cache each iteration"""
|
||||
|
||||
# Get API key
|
||||
api_key = os.getenv("MOONSHOT_API_KEY")
|
||||
if not api_key:
|
||||
print("❌ Please set MOONSHOT_API_KEY environment variable")
|
||||
sys.exit(1)
|
||||
|
||||
print("🔬 Testing KV Cache Invalidation")
|
||||
print("="*60)
|
||||
|
||||
# Simple task that requires multiple iterations
|
||||
task = "Find Python files in chapter1/context and tell me how many there are."
|
||||
|
||||
print(f"Task: {task}")
|
||||
print("-"*40)
|
||||
|
||||
# Test 1: CORRECT mode (should use cache)
|
||||
print("\n1️⃣ Testing CORRECT mode (should use cache):")
|
||||
agent_correct = KVCacheAgent(
|
||||
api_key=api_key,
|
||||
mode=KVCacheMode.CORRECT,
|
||||
root_dir="../..",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result_correct = agent_correct.execute_task(task, max_iterations=5)
|
||||
metrics_correct = result_correct["metrics"]
|
||||
|
||||
print(f"\n Results for CORRECT mode:")
|
||||
print(f" • Iterations: {result_correct['iterations']}")
|
||||
print(f" • TTFT per iteration: {[f'{t:.2f}s' for t in metrics_correct.ttft_per_iteration]}")
|
||||
print(f" • Cached tokens: {metrics_correct.cached_tokens}")
|
||||
print(f" • Cache hits: {metrics_correct.cache_hits}")
|
||||
|
||||
# Test 2: DYNAMIC_SYSTEM mode (should NOT use cache)
|
||||
print("\n2️⃣ Testing DYNAMIC_SYSTEM mode (should NOT use cache):")
|
||||
agent_dynamic = KVCacheAgent(
|
||||
api_key=api_key,
|
||||
mode=KVCacheMode.DYNAMIC_SYSTEM,
|
||||
root_dir="../..",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result_dynamic = agent_dynamic.execute_task(task, max_iterations=5)
|
||||
metrics_dynamic = result_dynamic["metrics"]
|
||||
|
||||
print(f"\n Results for DYNAMIC_SYSTEM mode:")
|
||||
print(f" • Iterations: {result_dynamic['iterations']}")
|
||||
print(f" • TTFT per iteration: {[f'{t:.2f}s' for t in metrics_dynamic.ttft_per_iteration]}")
|
||||
print(f" • Cached tokens: {metrics_dynamic.cached_tokens}")
|
||||
print(f" • Cache hits: {metrics_dynamic.cache_hits}")
|
||||
|
||||
# Analysis
|
||||
print("\n" + "="*60)
|
||||
print("📊 ANALYSIS:")
|
||||
print("-"*40)
|
||||
|
||||
# Check TTFT improvement
|
||||
if len(metrics_correct.ttft_per_iteration) > 1:
|
||||
correct_improvement = (metrics_correct.ttft_per_iteration[0] - metrics_correct.ttft_per_iteration[-1]) / metrics_correct.ttft_per_iteration[0] * 100
|
||||
print(f"CORRECT mode TTFT improvement: {correct_improvement:.1f}%")
|
||||
|
||||
if len(metrics_dynamic.ttft_per_iteration) > 1:
|
||||
dynamic_improvement = (metrics_dynamic.ttft_per_iteration[0] - metrics_dynamic.ttft_per_iteration[-1]) / metrics_dynamic.ttft_per_iteration[0] * 100
|
||||
print(f"DYNAMIC mode TTFT improvement: {dynamic_improvement:.1f}%")
|
||||
|
||||
# Verify cache behavior
|
||||
print("\n✅ Verification:")
|
||||
if metrics_correct.cached_tokens > 0:
|
||||
print(f" ✓ CORRECT mode used cache: {metrics_correct.cached_tokens} tokens")
|
||||
else:
|
||||
print(f" ✗ CORRECT mode did NOT use cache (unexpected!)")
|
||||
|
||||
if metrics_dynamic.cached_tokens == 0:
|
||||
print(f" ✓ DYNAMIC mode did NOT use cache (expected)")
|
||||
else:
|
||||
print(f" ✗ DYNAMIC mode used cache: {metrics_dynamic.cached_tokens} tokens (unexpected!)")
|
||||
|
||||
# Check TTFT consistency
|
||||
print("\n🔍 TTFT Consistency Check:")
|
||||
if len(metrics_correct.ttft_per_iteration) > 2:
|
||||
# CORRECT mode should show improvement after first iteration
|
||||
first_ttft = metrics_correct.ttft_per_iteration[0]
|
||||
avg_rest = sum(metrics_correct.ttft_per_iteration[1:]) / len(metrics_correct.ttft_per_iteration[1:])
|
||||
if avg_rest < first_ttft * 0.7: # At least 30% improvement
|
||||
print(f" ✓ CORRECT mode shows cache benefit (first: {first_ttft:.2f}s, avg rest: {avg_rest:.2f}s)")
|
||||
else:
|
||||
print(f" ⚠️ CORRECT mode improvement less than expected")
|
||||
|
||||
if len(metrics_dynamic.ttft_per_iteration) > 2:
|
||||
# DYNAMIC mode should NOT show significant improvement
|
||||
all_ttfts = metrics_dynamic.ttft_per_iteration
|
||||
min_ttft = min(all_ttfts)
|
||||
max_ttft = max(all_ttfts)
|
||||
if (max_ttft - min_ttft) / max_ttft < 0.3: # Less than 30% variation
|
||||
print(f" ✓ DYNAMIC mode shows consistent TTFT (no cache benefit)")
|
||||
else:
|
||||
print(f" ⚠️ DYNAMIC mode shows unexpected TTFT variation")
|
||||
|
||||
print("\n💡 Key Finding:")
|
||||
print("The CORRECT mode should show significant TTFT improvement after the first")
|
||||
print("iteration due to KV cache, while incorrect modes should maintain")
|
||||
print("consistently high TTFT because the cache is invalidated on each iteration.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_cache_invalidation()
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify cached tokens are being parsed correctly from Kimi API
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from _bootstrap import add_project_root
|
||||
|
||||
add_project_root()
|
||||
|
||||
from agent import KVCacheAgent, KVCacheMode
|
||||
|
||||
def test_cached_tokens():
|
||||
"""Test that cached tokens are correctly parsed from API response"""
|
||||
|
||||
# Get API key
|
||||
api_key = os.getenv("MOONSHOT_API_KEY")
|
||||
if not api_key:
|
||||
print("❌ Please set MOONSHOT_API_KEY environment variable")
|
||||
sys.exit(1)
|
||||
|
||||
print("🔍 Testing Cached Tokens Parsing")
|
||||
print("="*60)
|
||||
|
||||
# Simple task that requires a few iterations
|
||||
task = "Find Python files in chapter1/context directory and tell me how many there are."
|
||||
|
||||
print(f"Task: {task}")
|
||||
print("-"*40)
|
||||
|
||||
# Run with correct implementation (should use cache)
|
||||
print("\nRunning agent with CORRECT implementation...")
|
||||
agent = KVCacheAgent(
|
||||
api_key=api_key,
|
||||
mode=KVCacheMode.CORRECT,
|
||||
root_dir="../..",
|
||||
verbose=True # Enable verbose to see token logging
|
||||
)
|
||||
|
||||
result = agent.execute_task(task, max_iterations=5)
|
||||
metrics = result["metrics"]
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("📊 Cache Token Results:")
|
||||
print(f" • Total iterations: {result['iterations']}")
|
||||
print(f" • Cached tokens accumulated: {metrics.cached_tokens}")
|
||||
print(f" • Cache hits: {metrics.cache_hits}")
|
||||
print(f" • Cache misses: {metrics.cache_misses}")
|
||||
|
||||
# Check each iteration's TTFT
|
||||
if metrics.ttft_per_iteration:
|
||||
print(f"\n • TTFT per iteration:")
|
||||
for i, ttft in enumerate(metrics.ttft_per_iteration, 1):
|
||||
status = "🔴 No cache" if i == 1 else "🟢 With cache"
|
||||
print(f" Iteration {i}: {ttft:.3f}s {status}")
|
||||
|
||||
# Verify cache is working
|
||||
print("\n✅ Verification:")
|
||||
if metrics.cached_tokens > 0:
|
||||
print(f" ✓ Cached tokens detected: {metrics.cached_tokens}")
|
||||
else:
|
||||
print(f" ⚠️ No cached tokens detected - cache may not be working")
|
||||
|
||||
if len(metrics.ttft_per_iteration) > 1:
|
||||
first_ttft = metrics.ttft_per_iteration[0]
|
||||
second_ttft = metrics.ttft_per_iteration[1]
|
||||
if second_ttft < first_ttft * 0.8: # At least 20% improvement
|
||||
print(f" ✓ TTFT improved from {first_ttft:.3f}s to {second_ttft:.3f}s")
|
||||
else:
|
||||
print(f" ⚠️ TTFT did not improve significantly")
|
||||
|
||||
print("\n💡 Note: Kimi API should return cached_tokens in the usage object")
|
||||
print(" starting from the second iteration when context is stable.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_cached_tokens()
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify the agent correctly identifies final answers
|
||||
when no tool calls are made
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from _bootstrap import add_project_root
|
||||
|
||||
add_project_root()
|
||||
|
||||
from agent import KVCacheAgent, KVCacheMode
|
||||
|
||||
def test_completion_logic():
|
||||
"""Test that the agent correctly handles responses without tool calls as final answers"""
|
||||
|
||||
# Get API key
|
||||
api_key = os.getenv("MOONSHOT_API_KEY")
|
||||
if not api_key:
|
||||
print("❌ Please set MOONSHOT_API_KEY environment variable")
|
||||
sys.exit(1)
|
||||
|
||||
print("🧪 Testing Final Answer Detection")
|
||||
print("="*60)
|
||||
|
||||
# Test 1: Simple question that doesn't require tools
|
||||
print("\n1️⃣ Test: Simple question without tools")
|
||||
task1 = "What is 2 + 2? Just tell me the answer, no need to use any tools."
|
||||
|
||||
agent = KVCacheAgent(
|
||||
api_key=api_key,
|
||||
mode=KVCacheMode.CORRECT,
|
||||
root_dir="../..",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
result = agent.execute_task(task1, max_iterations=5)
|
||||
print(f" Task: {task1}")
|
||||
print(f" ✓ Completed in {result['iterations']} iteration(s)")
|
||||
print(f" ✓ Tool calls: {len(result['tool_calls'])}")
|
||||
print(f" ✓ Has final answer: {result['success']}")
|
||||
if result['final_answer']:
|
||||
print(f" Answer: {result['final_answer'][:100]}")
|
||||
|
||||
# Test 2: Question that requires tools
|
||||
print("\n2️⃣ Test: Question requiring tools")
|
||||
task2 = "How many Python files are in the chapter1/context directory?"
|
||||
|
||||
agent2 = KVCacheAgent(
|
||||
api_key=api_key,
|
||||
mode=KVCacheMode.CORRECT,
|
||||
root_dir="../..",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
result2 = agent2.execute_task(task2, max_iterations=5)
|
||||
print(f" Task: {task2}")
|
||||
print(f" ✓ Completed in {result2['iterations']} iteration(s)")
|
||||
print(f" ✓ Tool calls: {len(result2['tool_calls'])}")
|
||||
print(f" ✓ Has final answer: {result2['success']}")
|
||||
|
||||
if result2['tool_calls']:
|
||||
print(" Tools used:")
|
||||
for tc in result2['tool_calls']:
|
||||
print(f" • {tc.name}")
|
||||
|
||||
# Test 3: Multi-step task
|
||||
print("\n3️⃣ Test: Multi-step task")
|
||||
task3 = "Find Python files in chapter1/context, then tell me if there's a file named 'agent.py'"
|
||||
|
||||
agent3 = KVCacheAgent(
|
||||
api_key=api_key,
|
||||
mode=KVCacheMode.CORRECT,
|
||||
root_dir="../..",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
result3 = agent3.execute_task(task3, max_iterations=10)
|
||||
print(f" Task: {task3}")
|
||||
print(f" ✓ Completed in {result3['iterations']} iteration(s)")
|
||||
print(f" ✓ Tool calls: {len(result3['tool_calls'])}")
|
||||
print(f" ✓ Has final answer: {result3['success']}")
|
||||
|
||||
# Summary
|
||||
print("\n" + "="*60)
|
||||
print("📊 Summary:")
|
||||
print(f" • Test 1 (no tools): {result['iterations']} iterations, {len(result['tool_calls'])} tools")
|
||||
print(f" • Test 2 (with tools): {result2['iterations']} iterations, {len(result2['tool_calls'])} tools")
|
||||
print(f" • Test 3 (multi-step): {result3['iterations']} iterations, {len(result3['tool_calls'])} tools")
|
||||
|
||||
print("\n✅ The agent correctly:")
|
||||
print(" 1. Identifies final answers when no tools are needed")
|
||||
print(" 2. Uses tools when necessary to gather information")
|
||||
print(" 3. Provides final answer after tool execution")
|
||||
print("\nNo explicit 'final answer' keyword needed!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_completion_logic()
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify the updated agent works with standard OpenAI tool calling
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from _bootstrap import add_project_root
|
||||
|
||||
add_project_root()
|
||||
|
||||
from agent import KVCacheAgent, KVCacheMode
|
||||
|
||||
def test_tool_calling():
|
||||
"""Test that the agent correctly uses OpenAI tool calling format"""
|
||||
|
||||
# Get API key
|
||||
api_key = os.getenv("MOONSHOT_API_KEY")
|
||||
if not api_key:
|
||||
print("❌ Please set MOONSHOT_API_KEY environment variable")
|
||||
sys.exit(1)
|
||||
|
||||
print("🧪 Testing Standard OpenAI Tool Calling Format")
|
||||
print("="*60)
|
||||
|
||||
# Simple task that requires tool calls
|
||||
task = "Find all Python files in the chapter1/context directory and tell me how many there are."
|
||||
|
||||
print(f"📝 Task: {task}")
|
||||
print("-"*60)
|
||||
|
||||
# Create agent with correct implementation
|
||||
agent = KVCacheAgent(
|
||||
api_key=api_key,
|
||||
mode=KVCacheMode.CORRECT,
|
||||
root_dir="../..",
|
||||
verbose=True # Enable verbose to see tool calls
|
||||
)
|
||||
|
||||
# Execute task
|
||||
result = agent.execute_task(task, max_iterations=5)
|
||||
|
||||
# Check results
|
||||
print("\n" + "="*60)
|
||||
print("📊 Results:")
|
||||
print(f"✓ Success: {result['success']}")
|
||||
print(f"✓ Iterations: {result['iterations']}")
|
||||
print(f"✓ Tool Calls Made: {len(result['tool_calls'])}")
|
||||
|
||||
if result['tool_calls']:
|
||||
print("\n🔧 Tool Calls:")
|
||||
for tc in result['tool_calls']:
|
||||
print(f" • {tc.name}({tc.arguments})")
|
||||
if tc.result and tc.result.get('success'):
|
||||
if tc.name == 'find':
|
||||
print(f" → Found {tc.result.get('count', 0)} files")
|
||||
|
||||
if result['final_answer']:
|
||||
print(f"\n💬 Final Answer:")
|
||||
print(f" {result['final_answer'][:200]}...")
|
||||
|
||||
# Test metrics
|
||||
metrics = result['metrics']
|
||||
print(f"\n📈 Performance Metrics:")
|
||||
print(f" • TTFT: {metrics.ttft:.3f}s")
|
||||
print(f" • Total Time: {metrics.total_time:.3f}s")
|
||||
print(f" • Cached Tokens: {metrics.cached_tokens}")
|
||||
|
||||
print("\n✅ Tool calling test completed successfully!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_tool_calling()
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to demonstrate TTFT tracking across iterations
|
||||
Shows how cache usage improves response times
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from _bootstrap import add_project_root
|
||||
|
||||
add_project_root()
|
||||
|
||||
from agent import KVCacheAgent, KVCacheMode
|
||||
|
||||
def test_ttft_tracking():
|
||||
"""Test and display TTFT tracking across iterations"""
|
||||
|
||||
# Get API key
|
||||
api_key = os.getenv("MOONSHOT_API_KEY")
|
||||
if not api_key:
|
||||
print("❌ Please set MOONSHOT_API_KEY environment variable")
|
||||
sys.exit(1)
|
||||
|
||||
print("📊 TTFT Tracking Demonstration")
|
||||
print("="*60)
|
||||
|
||||
# Task that requires multiple iterations
|
||||
task = """Analyze the chapter1/context directory:
|
||||
1. Find all Python files
|
||||
2. Read the agent.py file (first 100 lines)
|
||||
3. Search for classes in the code
|
||||
4. Provide a summary of what you found"""
|
||||
|
||||
print(f"Task: {task[:100]}...")
|
||||
print("="*60)
|
||||
|
||||
# Test with correct implementation (should show cache benefits)
|
||||
print("\n✅ CORRECT Implementation (with KV cache):")
|
||||
print("-"*40)
|
||||
|
||||
agent = KVCacheAgent(
|
||||
api_key=api_key,
|
||||
mode=KVCacheMode.CORRECT,
|
||||
root_dir="../..",
|
||||
verbose=False # Set to True to see detailed logs
|
||||
)
|
||||
|
||||
result = agent.execute_task(task, max_iterations=10)
|
||||
metrics = result["metrics"]
|
||||
|
||||
# Display TTFT progression
|
||||
print(f"Iterations completed: {result['iterations']}")
|
||||
print(f"Tool calls made: {len(result['tool_calls'])}")
|
||||
print(f"\nTTFT per iteration:")
|
||||
|
||||
for i, ttft in enumerate(metrics.ttft_per_iteration, 1):
|
||||
bar_length = int(ttft * 10) # Visual bar representation
|
||||
bar = "█" * min(bar_length, 50)
|
||||
print(f" Iter {i:2d}: {ttft:6.3f}s {bar}")
|
||||
|
||||
# Calculate statistics
|
||||
if len(metrics.ttft_per_iteration) > 1:
|
||||
first = metrics.ttft_per_iteration[0]
|
||||
last = metrics.ttft_per_iteration[-1]
|
||||
avg_all = sum(metrics.ttft_per_iteration) / len(metrics.ttft_per_iteration)
|
||||
avg_after_first = sum(metrics.ttft_per_iteration[1:]) / len(metrics.ttft_per_iteration[1:])
|
||||
|
||||
print(f"\n📈 Performance Analysis:")
|
||||
print(f" • First iteration: {first:.3f}s (cold start)")
|
||||
print(f" • Last iteration: {last:.3f}s")
|
||||
print(f" • Average (all): {avg_all:.3f}s")
|
||||
print(f" • Average (cached): {avg_after_first:.3f}s")
|
||||
print(f" • Speed improvement: {(first - last) / first * 100:.1f}%")
|
||||
print(f" • Cached tokens: {metrics.cached_tokens:,}")
|
||||
|
||||
# Compare with dynamic system prompt (no cache benefits)
|
||||
print("\n" + "="*60)
|
||||
print("❌ DYNAMIC SYSTEM Implementation (breaks KV cache):")
|
||||
print("-"*40)
|
||||
|
||||
agent2 = KVCacheAgent(
|
||||
api_key=api_key,
|
||||
mode=KVCacheMode.DYNAMIC_SYSTEM,
|
||||
root_dir="../..",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
result2 = agent2.execute_task(task, max_iterations=10)
|
||||
metrics2 = result2["metrics"]
|
||||
|
||||
print(f"Iterations completed: {result2['iterations']}")
|
||||
print(f"Tool calls made: {len(result2['tool_calls'])}")
|
||||
print(f"\nTTFT per iteration:")
|
||||
|
||||
for i, ttft in enumerate(metrics2.ttft_per_iteration, 1):
|
||||
bar_length = int(ttft * 10)
|
||||
bar = "█" * min(bar_length, 50)
|
||||
print(f" Iter {i:2d}: {ttft:6.3f}s {bar}")
|
||||
|
||||
if len(metrics2.ttft_per_iteration) > 1:
|
||||
first2 = metrics2.ttft_per_iteration[0]
|
||||
last2 = metrics2.ttft_per_iteration[-1]
|
||||
avg_all2 = sum(metrics2.ttft_per_iteration) / len(metrics2.ttft_per_iteration)
|
||||
|
||||
print(f"\n📉 Performance Analysis:")
|
||||
print(f" • First iteration: {first2:.3f}s")
|
||||
print(f" • Last iteration: {last2:.3f}s")
|
||||
print(f" • Average (all): {avg_all2:.3f}s")
|
||||
print(f" • Speed improvement: {(first2 - last2) / first2 * 100:.1f}% (minimal)")
|
||||
print(f" • Cached tokens: {metrics2.cached_tokens:,} (should be 0)")
|
||||
|
||||
# Comparison
|
||||
print("\n" + "="*60)
|
||||
print("🔬 COMPARISON:")
|
||||
print("-"*40)
|
||||
|
||||
if metrics.ttft_per_iteration and metrics2.ttft_per_iteration:
|
||||
avg1 = sum(metrics.ttft_per_iteration) / len(metrics.ttft_per_iteration)
|
||||
avg2 = sum(metrics2.ttft_per_iteration) / len(metrics2.ttft_per_iteration)
|
||||
|
||||
print(f"Average TTFT:")
|
||||
print(f" • Correct (with cache): {avg1:.3f}s")
|
||||
print(f" • Dynamic (no cache): {avg2:.3f}s")
|
||||
print(f" • Difference: {avg2 - avg1:.3f}s slower without cache")
|
||||
print(f" • Performance penalty: {(avg2 - avg1) / avg1 * 100:.1f}% slower")
|
||||
|
||||
print(f"\nCache Usage:")
|
||||
print(f" • Correct: {metrics.cached_tokens:,} tokens cached")
|
||||
print(f" • Dynamic: {metrics2.cached_tokens:,} tokens cached")
|
||||
|
||||
print("\n💡 Key Observation:")
|
||||
print("The correct implementation shows significant TTFT improvement after the")
|
||||
print("first iteration due to KV cache, while dynamic system prompt maintains")
|
||||
print("consistently high TTFT because the cache is invalidated on each request.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_ttft_tracking()
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick demonstration of KV cache impact
|
||||
Shows the difference between correct and incorrect implementations
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from _bootstrap import add_project_root
|
||||
|
||||
add_project_root()
|
||||
|
||||
from agent import KVCacheAgent, KVCacheMode
|
||||
|
||||
from agentbook.providers import PROVIDERS
|
||||
|
||||
def main():
|
||||
"""Run a quick demo comparing correct vs incorrect implementation"""
|
||||
|
||||
# Get API key. 优先 Moonshot/Kimi;缺失时回退 OPENROUTER_API_KEY
|
||||
# (KVCacheAgent 会自动切换到 OpenRouter 端点并映射模型名)。
|
||||
# 接受哪些环境变量由 agentbook 的 provider 注册表定义。
|
||||
api_key = PROVIDERS["kimi"].api_key() or os.getenv("OPENROUTER_API_KEY")
|
||||
if not api_key:
|
||||
print("❌ Please set MOONSHOT_API_KEY (or KIMI_API_KEY / OPENROUTER_API_KEY)")
|
||||
print(" export MOONSHOT_API_KEY='your-api-key-here'")
|
||||
sys.exit(1)
|
||||
|
||||
print("🚀 KV Cache Quick Demo")
|
||||
print("="*60)
|
||||
|
||||
# Simple task that requires multiple tool calls
|
||||
task = """Please do the following:
|
||||
1. Find all Python files in the chapter1 directory
|
||||
2. Read the main.py file from the context project
|
||||
3. Search for the word 'agent' in chapter1 files
|
||||
4. Provide a brief summary of what you found"""
|
||||
|
||||
print(f"📝 Task: {task}")
|
||||
print("="*60)
|
||||
|
||||
# Test 1: Correct implementation
|
||||
print("\n✅ Testing CORRECT implementation (with KV cache)...")
|
||||
print("-"*60)
|
||||
agent_correct = KVCacheAgent(
|
||||
api_key=api_key,
|
||||
mode=KVCacheMode.CORRECT,
|
||||
root_dir="../..",
|
||||
verbose=False # Set to True for detailed logs
|
||||
)
|
||||
|
||||
result_correct = agent_correct.execute_task(task, max_iterations=10)
|
||||
metrics_correct = result_correct["metrics"]
|
||||
|
||||
print(f"✓ TTFT: {metrics_correct.ttft:.3f}s")
|
||||
print(f"✓ Total Time: {metrics_correct.total_time:.3f}s")
|
||||
print(f"✓ Cached Tokens: {metrics_correct.cached_tokens:,}")
|
||||
print(f"✓ Cache Hits: {metrics_correct.cache_hits}")
|
||||
print(f"✓ Total Tokens Used: {metrics_correct.prompt_tokens + metrics_correct.completion_tokens:,}")
|
||||
|
||||
# Test 2: Incorrect implementation (dynamic system prompt)
|
||||
print("\n❌ Testing INCORRECT implementation (dynamic system prompt)...")
|
||||
print("-"*60)
|
||||
agent_incorrect = KVCacheAgent(
|
||||
api_key=api_key,
|
||||
mode=KVCacheMode.DYNAMIC_SYSTEM,
|
||||
root_dir="../..",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
result_incorrect = agent_incorrect.execute_task(task, max_iterations=10)
|
||||
metrics_incorrect = result_incorrect["metrics"]
|
||||
|
||||
print(f"✗ TTFT: {metrics_incorrect.ttft:.3f}s")
|
||||
print(f"✗ Total Time: {metrics_incorrect.total_time:.3f}s")
|
||||
print(f"✗ Cached Tokens: {metrics_incorrect.cached_tokens:,}")
|
||||
print(f"✗ Cache Hits: {metrics_incorrect.cache_hits}")
|
||||
print(f"✗ Total Tokens Used: {metrics_incorrect.prompt_tokens + metrics_incorrect.completion_tokens:,}")
|
||||
|
||||
# Comparison
|
||||
print("\n📊 Performance Impact:")
|
||||
print("="*60)
|
||||
|
||||
ttft_diff = ((metrics_incorrect.ttft - metrics_correct.ttft) / metrics_correct.ttft) * 100
|
||||
time_diff = ((metrics_incorrect.total_time - metrics_correct.total_time) / metrics_correct.total_time) * 100
|
||||
cache_lost = metrics_correct.cached_tokens - metrics_incorrect.cached_tokens
|
||||
|
||||
print(f"⚡ TTFT increased by: {ttft_diff:.1f}%")
|
||||
print(f"⏱️ Total time increased by: {time_diff:.1f}%")
|
||||
print(f"💾 Cache tokens lost: {cache_lost:,}")
|
||||
|
||||
if ttft_diff > 50:
|
||||
print("\n⚠️ Dynamic system prompts severely impact performance!")
|
||||
print(" Even small context changes can invalidate the entire KV cache.")
|
||||
|
||||
print("\n💡 Key Takeaway:")
|
||||
print(" Maintaining stable context is crucial for LLM performance.")
|
||||
print(" Small implementation details can have major performance impacts!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline regressions for local tool error handling."""
|
||||
|
||||
from agent import LocalFileTools
|
||||
|
||||
def test_error_handling():
|
||||
"""Test that local tools return structured errors instead of raising."""
|
||||
|
||||
print("🧪 Testing Error Handling in Tool Execution")
|
||||
print("="*60)
|
||||
|
||||
# Test local tools directly first
|
||||
print("\n1️⃣ Testing direct tool error handling:")
|
||||
tools = LocalFileTools(root_dir="../..")
|
||||
|
||||
# Test with invalid arguments
|
||||
print(" Testing read_file with extra 'limit' parameter...")
|
||||
# The tool should ignore the extra parameter
|
||||
result = tools.read_file("chapter1/context/README.md")
|
||||
print(f" Result: {'✓ Success' if result.get('success') else '✗ Error'}")
|
||||
assert result.get("success") is True
|
||||
|
||||
# Test with non-existent file
|
||||
print(" Testing read_file with non-existent file...")
|
||||
result = tools.read_file("non_existent_file.txt")
|
||||
print(f" Result: {'✓ Error handled' if not result.get('success') else '✗ Unexpected success'}")
|
||||
print(f" Error message: {result.get('error', 'N/A')}")
|
||||
assert result.get("success") is False
|
||||
assert "File not found" in result.get("error", "")
|
||||
|
||||
# Test security boundary
|
||||
print(" Testing security boundary...")
|
||||
result = tools.read_file("../../../../etc/passwd")
|
||||
print(f" Result: {'✓ Access denied' if 'Access denied' in result.get('error', '') else '✗ Security issue'}")
|
||||
assert result.get("success") is False
|
||||
assert "Access denied" in result.get("error", "")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("✅ Error handling test complete!")
|
||||
print("\nKey findings:")
|
||||
print(" • Tools return errors as results instead of throwing exceptions")
|
||||
print(" • Unexpected arguments are filtered out safely")
|
||||
print(" • Security boundaries are enforced")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_error_handling()
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for the read_file tool with offset and size parameters
|
||||
"""
|
||||
|
||||
from agent import LocalFileTools
|
||||
|
||||
def test_file_range_reading():
|
||||
"""Test reading files with offset and size parameters"""
|
||||
|
||||
print("🧪 Testing File Range Reading")
|
||||
print("="*60)
|
||||
|
||||
# Initialize tools
|
||||
tools = LocalFileTools(root_dir="../..")
|
||||
|
||||
# Test file
|
||||
test_file = "chapter1/context/agent.py"
|
||||
|
||||
# Test 1: Read first 10 lines
|
||||
print("\n1️⃣ Reading first 10 lines:")
|
||||
result = tools.read_file(test_file, offset=0, size=10)
|
||||
if result["success"]:
|
||||
print(f" ✓ Read {result['lines_read']} lines from total {result['total_lines']}")
|
||||
print(f" Range: lines {result['offset']}-{result['end_line']}")
|
||||
print(f" First line: {result['content'].split(chr(10))[0][:50]}...")
|
||||
else:
|
||||
print(f" ✗ Error: {result['error']}")
|
||||
|
||||
# Test 2: Read lines 100-110
|
||||
print("\n2️⃣ Reading lines 100-110:")
|
||||
result = tools.read_file(test_file, offset=100, size=10)
|
||||
if result["success"]:
|
||||
print(f" ✓ Read {result['lines_read']} lines")
|
||||
print(f" Range: lines {result['offset']}-{result['end_line']}")
|
||||
lines = result['content'].split('\n')
|
||||
if lines:
|
||||
print(f" Sample: {lines[0][:60]}...")
|
||||
else:
|
||||
print(f" ✗ Error: {result['error']}")
|
||||
|
||||
# Test 3: Read from offset 250 with size 500 (as specified)
|
||||
print("\n3️⃣ Reading from offset 250, size 500:")
|
||||
result = tools.read_file(test_file, offset=250, size=500)
|
||||
if result["success"]:
|
||||
print(f" ✓ Read {result['lines_read']} lines")
|
||||
print(f" Range: lines {result['offset']}-{result['end_line']}")
|
||||
print(f" Total file has {result['total_lines']} lines")
|
||||
else:
|
||||
print(f" ✗ Error: {result['error']}")
|
||||
|
||||
# Test 4: Read without size (from offset to end)
|
||||
print("\n4️⃣ Reading from offset 700 to end:")
|
||||
result = tools.read_file(test_file, offset=700)
|
||||
if result["success"]:
|
||||
print(f" ✓ Read {result['lines_read']} lines")
|
||||
print(f" Range: lines {result['offset']}-{result['end_line']}")
|
||||
else:
|
||||
print(f" ✗ Error: {result['error']}")
|
||||
|
||||
# Test 5: Offset beyond file length
|
||||
print("\n5️⃣ Testing offset beyond file length:")
|
||||
result = tools.read_file(test_file, offset=10000, size=10)
|
||||
if result["success"]:
|
||||
print(f" ✓ Handled gracefully: {result.get('message', 'No error')}")
|
||||
print(f" Lines read: {result['lines_read']}")
|
||||
else:
|
||||
print(f" Result: {result}")
|
||||
|
||||
# Test 6: Read entire file (no offset, no size)
|
||||
print("\n6️⃣ Reading entire file (default behavior):")
|
||||
result = tools.read_file("chapter1/context/README.md")
|
||||
if result["success"]:
|
||||
print(f" ✓ Read entire file")
|
||||
print(f" Total lines: {result['total_lines']}")
|
||||
print(f" Lines read: {result['lines_read']}")
|
||||
print(f" Truncated: {result.get('truncated', False)}")
|
||||
else:
|
||||
print(f" ✗ Error: {result['error']}")
|
||||
|
||||
# Test 7: Compare with limit parameter (the user's original request)
|
||||
print("\n7️⃣ API-style usage (offset=250, size=500):")
|
||||
result = tools.read_file("chapter2/local_llm_serving/main.py", offset=250, size=500)
|
||||
if result["success"]:
|
||||
print(f" ✓ Successfully read lines {result['offset']}-{result['end_line']}")
|
||||
print(f" Lines read: {result['lines_read']}")
|
||||
print(f" File has {result['total_lines']} total lines")
|
||||
|
||||
# Show a sample of the content
|
||||
lines = result['content'].split('\n')[:3]
|
||||
print("\n First 3 lines of content:")
|
||||
for i, line in enumerate(lines):
|
||||
print(f" Line {250+i}: {line[:60]}..." if len(line) > 60 else f" Line {250+i}: {line}")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("✅ File range reading tests complete!")
|
||||
print("\nThe read_file tool now supports:")
|
||||
print(" • offset: Starting line number (0-based)")
|
||||
print(" • size: Number of lines to read")
|
||||
print(" • Handles edge cases gracefully")
|
||||
print(" • Maintains security boundaries")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_file_range_reading()
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for interactive mode selection
|
||||
"""
|
||||
|
||||
from main import select_mode_interactive
|
||||
|
||||
def test_mode_selection(monkeypatch):
|
||||
"""Test the interactive mode selection without running the agent"""
|
||||
|
||||
print("🧪 Testing Interactive Mode Selection")
|
||||
print("(This is a test - no agent will actually run)")
|
||||
|
||||
# Test the selection menu
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt: "7")
|
||||
selected = select_mode_interactive()
|
||||
assert selected == "compare"
|
||||
|
||||
print("\n" + "="*60)
|
||||
if selected == "compare":
|
||||
print("✅ You selected: Compare all modes")
|
||||
print("In real usage, this would run all 6 implementations and compare them.")
|
||||
else:
|
||||
print(f"✅ You selected: {selected}")
|
||||
print(f"In real usage, this would run the '{selected}' implementation.")
|
||||
|
||||
print("\nTest complete!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_mode_selection()
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify message flow in correct vs incorrect modes
|
||||
"""
|
||||
|
||||
def test_message_flow_logic():
|
||||
"""Simulate how messages are handled in different modes"""
|
||||
|
||||
print("🔍 Testing Message Flow Logic")
|
||||
print("="*60)
|
||||
|
||||
# Simulate CORRECT mode
|
||||
print("\n✅ CORRECT Mode:")
|
||||
print("-"*40)
|
||||
|
||||
messages_correct = None
|
||||
history_correct = []
|
||||
|
||||
for iteration in range(1, 4):
|
||||
print(f"\nIteration {iteration}:")
|
||||
|
||||
if iteration == 1:
|
||||
# First iteration: create messages
|
||||
messages_correct = ["system", "task"]
|
||||
print(f" • Created messages: {messages_correct}")
|
||||
else:
|
||||
print(f" • Using existing messages: {messages_correct}")
|
||||
|
||||
# Simulate tool call
|
||||
print(f" • API returns tool call")
|
||||
messages_correct.append(f"assistant_iter{iteration}")
|
||||
history_correct.append(f"assistant_iter{iteration}")
|
||||
|
||||
# Simulate tool result
|
||||
print(f" • Tool executed")
|
||||
messages_correct.append(f"tool_result_iter{iteration}")
|
||||
history_correct.append(f"tool_result_iter{iteration}")
|
||||
|
||||
print(f" • Messages now: {messages_correct}")
|
||||
print(f" • History now: {history_correct}")
|
||||
|
||||
# Simulate INCORRECT mode
|
||||
print("\n\n❌ INCORRECT Mode (e.g., DYNAMIC_SYSTEM):")
|
||||
print("-"*40)
|
||||
|
||||
history_incorrect = []
|
||||
|
||||
for iteration in range(1, 4):
|
||||
print(f"\nIteration {iteration}:")
|
||||
|
||||
# Always recreate messages from history
|
||||
messages_incorrect = ["system_with_timestamp", "task"] + history_incorrect
|
||||
print(f" • Recreated messages: {messages_incorrect}")
|
||||
|
||||
# Simulate tool call
|
||||
print(f" • API returns tool call")
|
||||
messages_incorrect.append(f"assistant_iter{iteration}")
|
||||
history_incorrect.append(f"assistant_iter{iteration}")
|
||||
|
||||
# Simulate tool result
|
||||
print(f" • Tool executed")
|
||||
messages_incorrect.append(f"tool_result_iter{iteration}")
|
||||
history_incorrect.append(f"tool_result_iter{iteration}")
|
||||
|
||||
print(f" • Messages now: {messages_incorrect}")
|
||||
print(f" • History now: {history_incorrect}")
|
||||
|
||||
print("\n\n📊 Key Observations:")
|
||||
print("="*60)
|
||||
print("\n1. CORRECT Mode:")
|
||||
print(" • Messages list persists across iterations")
|
||||
print(" • Each iteration adds to the same list")
|
||||
print(" • Context remains stable → KV cache works")
|
||||
|
||||
print("\n2. INCORRECT Mode:")
|
||||
print(" • Messages list recreated each iteration")
|
||||
print(" • System prompt changes (timestamp)")
|
||||
print(" • Context changes → KV cache invalidated")
|
||||
|
||||
print("\n3. Both Modes:")
|
||||
print(" • Within an iteration, tool results are appended")
|
||||
print(" • This ensures the API sees complete conversation")
|
||||
print(" • History is maintained for reconstruction")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_message_flow_logic()
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Regression: negative size must read to EOF, not drop a suffix."""
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _stub():
|
||||
try:
|
||||
import openai # noqa: F401
|
||||
except ImportError:
|
||||
sys.modules.setdefault("openai", types.ModuleType("openai"))
|
||||
sys.modules["openai"].OpenAI = object
|
||||
|
||||
|
||||
_stub()
|
||||
|
||||
from agent import LocalFileTools # noqa: E402
|
||||
|
||||
|
||||
def test_negative_size_reads_all(tmp_path: Path):
|
||||
(tmp_path / "a.txt").write_text("a\nb\nc\n", encoding="utf-8")
|
||||
tools = LocalFileTools(str(tmp_path))
|
||||
out = tools.read_file("a.txt", offset=0, size=-1)
|
||||
assert out["success"] is True
|
||||
assert out["content"] == "a\nb\nc\n"
|
||||
assert out["lines_read"] == 3
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for local file system tools
|
||||
Validates that read_file, find, and grep work correctly
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from agent import LocalFileTools
|
||||
|
||||
|
||||
def test_file_tools():
|
||||
"""Test the local file system tools"""
|
||||
|
||||
print("🧪 Testing Local File System Tools")
|
||||
print("="*60)
|
||||
|
||||
# Initialize tools with project root
|
||||
tools = LocalFileTools(root_dir="../..")
|
||||
|
||||
# Test 1: Find Python files
|
||||
print("\n1️⃣ Testing 'find' command...")
|
||||
print(" Finding *.py files in chapter1/context directory...")
|
||||
result = tools.find("*.py", "chapter1/context")
|
||||
|
||||
if result["success"]:
|
||||
print(f" ✓ Found {result['count']} Python files")
|
||||
if result["matches"]:
|
||||
print(f" Sample files: {result['matches'][:3]}")
|
||||
else:
|
||||
print(f" ✗ Error: {result['error']}")
|
||||
|
||||
# Test 2: Read a file
|
||||
print("\n2️⃣ Testing 'read_file' command...")
|
||||
test_file = "chapter1/context/README.md"
|
||||
print(f" Reading {test_file}...")
|
||||
result = tools.read_file(test_file)
|
||||
|
||||
if result["success"]:
|
||||
print(f" ✓ Read file successfully ({len(result['content'])} bytes)")
|
||||
print(f" First 100 chars: {result['content'][:100]}...")
|
||||
else:
|
||||
print(f" ✗ Error: {result['error']}")
|
||||
|
||||
# Test 3: Grep for a pattern
|
||||
print("\n3️⃣ Testing 'grep' command...")
|
||||
print(" Searching for 'agent' in chapter1 directory...")
|
||||
result = tools.grep("agent", directory="chapter1")
|
||||
|
||||
if result["success"]:
|
||||
print(f" ✓ Found {result['match_count']} matches in {result['files_searched']} files")
|
||||
if result["matches"]:
|
||||
sample = result["matches"][0]
|
||||
print(f" Sample match: {sample['file']}:{sample['line_num']} - {sample['line'][:50]}...")
|
||||
else:
|
||||
print(f" ✗ Error: {result['error']}")
|
||||
|
||||
# Test 4: Security check - try to access outside root
|
||||
print("\n4️⃣ Testing security boundaries...")
|
||||
print(" Attempting to read file outside root directory...")
|
||||
result = tools.read_file("../../../../../../etc/passwd")
|
||||
|
||||
if not result["success"] and "Access denied" in result.get("error", ""):
|
||||
print(" ✓ Security check passed - access denied as expected")
|
||||
else:
|
||||
print(" ⚠️ Security check result:", result.get("error", "Unexpected result"))
|
||||
|
||||
# Test 5: Grep in specific file
|
||||
print("\n5️⃣ Testing 'grep' in specific file...")
|
||||
print(" Searching for 'class' in chapter1/context/agent.py...")
|
||||
result = tools.grep("class", file_path="chapter1/context/agent.py")
|
||||
|
||||
if result["success"]:
|
||||
print(f" ✓ Found {result['match_count']} matches")
|
||||
if result["matches"]:
|
||||
for match in result["matches"][:3]:
|
||||
print(f" Line {match['line_num']}: {match['line'][:60]}...")
|
||||
else:
|
||||
print(f" ✗ Error: {result['error']}")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("✅ Tool testing complete!")
|
||||
print("\nAll tools are working correctly and can be used by the ReAct agent.")
|
||||
print("Security boundaries are properly enforced.")
|
||||
|
||||
|
||||
def test_pattern_matching():
|
||||
"""Test various pattern matching scenarios"""
|
||||
|
||||
print("\n🔍 Testing Pattern Matching Capabilities")
|
||||
print("="*60)
|
||||
|
||||
tools = LocalFileTools(root_dir="../..")
|
||||
|
||||
# Test different file patterns
|
||||
patterns = [
|
||||
("*.md", "chapter1", "Markdown files"),
|
||||
("*.py", "chapter2", "Python files"),
|
||||
("README*", ".", "README files"),
|
||||
("test_*.py", "chapter1", "Test files"),
|
||||
]
|
||||
|
||||
for pattern, directory, description in patterns:
|
||||
print(f"\n• Finding {description}: {pattern} in {directory}")
|
||||
result = tools.find(pattern, directory)
|
||||
if result["success"]:
|
||||
print(f" Found {result['count']} files")
|
||||
else:
|
||||
print(f" Error: {result['error']}")
|
||||
|
||||
# Test different grep patterns
|
||||
print("\n📝 Testing Grep Patterns")
|
||||
print("-"*40)
|
||||
|
||||
grep_tests = [
|
||||
(r"def \w+\(", "chapter1/context/agent.py", "Function definitions"),
|
||||
(r"import \w+", "chapter1/context/main.py", "Import statements"),
|
||||
(r"TODO|FIXME", "chapter1", "TODO/FIXME comments"),
|
||||
(r"^\s*class", "chapter1/context/agent.py", "Class definitions"),
|
||||
]
|
||||
|
||||
for pattern, target, description in grep_tests:
|
||||
print(f"\n• Searching for {description}: {pattern}")
|
||||
if "/" in target:
|
||||
result = tools.grep(pattern, file_path=target)
|
||||
else:
|
||||
result = tools.grep(pattern, directory=target)
|
||||
|
||||
if result["success"]:
|
||||
print(f" Found {result['match_count']} matches")
|
||||
else:
|
||||
print(f" Error: {result['error']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run basic tests
|
||||
test_file_tools()
|
||||
|
||||
# Run pattern matching tests
|
||||
test_pattern_matching()
|
||||
|
||||
print("\n🎉 All tests completed successfully!")
|
||||
Reference in New Issue
Block a user