kittycode 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kittycode-0.1.0/.gitignore +19 -0
- kittycode-0.1.0/LICENSE +21 -0
- kittycode-0.1.0/PKG-INFO +13 -0
- kittycode-0.1.0/README.md +186 -0
- kittycode-0.1.0/README_CN.md +186 -0
- kittycode-0.1.0/kittycode/__init__.py +10 -0
- kittycode-0.1.0/kittycode/__main__.py +3 -0
- kittycode-0.1.0/kittycode/agent.py +125 -0
- kittycode-0.1.0/kittycode/cli.py +367 -0
- kittycode-0.1.0/kittycode/config.py +67 -0
- kittycode-0.1.0/kittycode/context.py +170 -0
- kittycode-0.1.0/kittycode/llm.py +325 -0
- kittycode-0.1.0/kittycode/prompt.py +51 -0
- kittycode-0.1.0/kittycode/session.py +66 -0
- kittycode-0.1.0/kittycode/skills.py +125 -0
- kittycode-0.1.0/kittycode/tools/__init__.py +27 -0
- kittycode-0.1.0/kittycode/tools/agent.py +47 -0
- kittycode-0.1.0/kittycode/tools/base.py +25 -0
- kittycode-0.1.0/kittycode/tools/bash.py +100 -0
- kittycode-0.1.0/kittycode/tools/edit.py +74 -0
- kittycode-0.1.0/kittycode/tools/glob_tool.py +42 -0
- kittycode-0.1.0/kittycode/tools/grep.py +70 -0
- kittycode-0.1.0/kittycode/tools/read.py +49 -0
- kittycode-0.1.0/kittycode/tools/write.py +37 -0
- kittycode-0.1.0/pyproject.toml +25 -0
- kittycode-0.1.0/tests/test_core.py +136 -0
- kittycode-0.1.0/tests/test_llm.py +90 -0
- kittycode-0.1.0/tests/test_skills.py +193 -0
- kittycode-0.1.0/tests/test_tools.py +183 -0
kittycode-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jimmy Ye
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
kittycode-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kittycode
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Minimal AI coding agent with a simple tool loop.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: anthropic>=0.84.0
|
|
9
|
+
Requires-Dist: openai>=1.0
|
|
10
|
+
Requires-Dist: prompt-toolkit>=3.0
|
|
11
|
+
Requires-Dist: rich>=13.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# KittyCode
|
|
2
|
+
|
|
3
|
+
KittyCode is a minimal terminal coding agent focused on a compact, readable implementation. It keeps the core runtime straightforward, with an agent loop, local tools, context compression, a small command-line interface, and support for both OpenAI-compatible and Anthropic APIs.
|
|
4
|
+
|
|
5
|
+
## Background
|
|
6
|
+
|
|
7
|
+
KittyCode follows a simple terminal-agent runtime model:
|
|
8
|
+
|
|
9
|
+
- A user message is sent through the configured model interface.
|
|
10
|
+
- The model can either answer directly or call tools.
|
|
11
|
+
- Tool calls are executed locally and the results are fed back into the conversation.
|
|
12
|
+
- The loop continues until the model returns plain text.
|
|
13
|
+
|
|
14
|
+
The project is intentionally small. It includes the core agent runtime, a compact CLI, session persistence, context compression, and a default built-in tool set.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- Minimal agent loop with optional parallel execution for multiple tool calls.
|
|
19
|
+
- LLM adapter that supports both OpenAI-compatible and Anthropic interfaces.
|
|
20
|
+
- Built-in tools for shell commands, file reading, file writing, targeted editing, glob search, regex search, and sub-agents.
|
|
21
|
+
- Startup skill discovery from `~/.kittycode/skills`, with skill metadata injected into the system prompt each round.
|
|
22
|
+
- Interactive REPL and one-shot command mode.
|
|
23
|
+
- Context compression to keep long sessions manageable.
|
|
24
|
+
- Session save and resume support.
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
- Python 3.10 or newer
|
|
29
|
+
- An API key for either an OpenAI-compatible endpoint or an Anthropic-compatible endpoint
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Clone the repository and install it in editable mode:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd KittyCode
|
|
37
|
+
python -m pip install -e .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If you also want the development test dependency:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python -m pip install -e .[dev]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
KittyCode reads startup configuration from `~/.kittycode/config.json`.
|
|
49
|
+
|
|
50
|
+
Supported fields:
|
|
51
|
+
|
|
52
|
+
- `interface`: `openai` or `anthropic`
|
|
53
|
+
- `api_key`
|
|
54
|
+
- `model`
|
|
55
|
+
- `base_url`
|
|
56
|
+
- `max_tokens`
|
|
57
|
+
- `temperature`
|
|
58
|
+
- `max_context`
|
|
59
|
+
|
|
60
|
+
OpenAI-compatible example:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"interface": "openai",
|
|
65
|
+
"api_key": "sk-...",
|
|
66
|
+
"model": "gpt-4o",
|
|
67
|
+
"base_url": "https://api.openai.com/v1",
|
|
68
|
+
"max_tokens": 4096,
|
|
69
|
+
"temperature": 0,
|
|
70
|
+
"max_context": 128000
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Anthropic example:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"interface": "anthropic",
|
|
79
|
+
"api_key": "sk-ant-...",
|
|
80
|
+
"model": "claude-3-7-sonnet-latest",
|
|
81
|
+
"base_url": "https://api.anthropic.com",
|
|
82
|
+
"max_tokens": 4096,
|
|
83
|
+
"temperature": 0,
|
|
84
|
+
"max_context": 128000
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The CLI still allows explicit overrides such as `--model`, `--interface`, `--base-url`, and `--api-key`.
|
|
89
|
+
|
|
90
|
+
## Skills
|
|
91
|
+
|
|
92
|
+
At startup, KittyCode scans `~/.kittycode/skills` for skill folders. Each skill should live in its own directory and include a `SKILL.md` file at the top level.
|
|
93
|
+
|
|
94
|
+
Expected layout:
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
~/.kittycode/skills/
|
|
98
|
+
example-skill/
|
|
99
|
+
SKILL.md
|
|
100
|
+
other-files...
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
KittyCode reads the leading `name` and `description` fields from each `SKILL.md`, keeps the resulting skill list in memory, and inserts the list into the system prompt at the start of every round. The prompt includes:
|
|
104
|
+
|
|
105
|
+
- `name`
|
|
106
|
+
- `description`
|
|
107
|
+
- `path`
|
|
108
|
+
|
|
109
|
+
This allows the model to see which local skills are available and decide when to read and use them.
|
|
110
|
+
|
|
111
|
+
Before each round, KittyCode checks whether the skill directory changed and reloads the cached skill metadata when needed, so adding or editing skills does not require restarting the process.
|
|
112
|
+
|
|
113
|
+
You can also invoke a loaded skill directly from the CLI with `/<skill name>`.
|
|
114
|
+
|
|
115
|
+
- `/<skill name>` selects that skill for your next non-command message.
|
|
116
|
+
- `/<skill name> <task>` runs the next request immediately with that skill.
|
|
117
|
+
|
|
118
|
+
## Usage
|
|
119
|
+
|
|
120
|
+
Run the interactive terminal UI:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
kittycode
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
You can also use the module entry point:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
python -m kittycode
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Run a one-shot prompt and exit:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
kittycode -p "Explain the project structure"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Resume a saved session:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
kittycode -r session_1234567890
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Override model, interface, or endpoint from the command line:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
kittycode --interface anthropic --model claude-3-7-sonnet-latest
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Interactive Commands
|
|
151
|
+
|
|
152
|
+
Inside the REPL, KittyCode supports:
|
|
153
|
+
|
|
154
|
+
- `/help`
|
|
155
|
+
- `/reset`
|
|
156
|
+
- `/skills`
|
|
157
|
+
- `/<skill name>`
|
|
158
|
+
- `/model <name>`
|
|
159
|
+
- `/tokens`
|
|
160
|
+
- `/compact`
|
|
161
|
+
- `/save`
|
|
162
|
+
- `/sessions`
|
|
163
|
+
- `/quit`
|
|
164
|
+
|
|
165
|
+
The `/skills` command refreshes the local skill cache if the skill directory has changed and then prints the currently loaded skills.
|
|
166
|
+
Slash commands also support prefix matching while typing, so entering `/` shows matching commands and skills through completion suggestions.
|
|
167
|
+
|
|
168
|
+
## Project Layout
|
|
169
|
+
|
|
170
|
+
- `kittycode/agent.py`: core agent loop
|
|
171
|
+
- `kittycode/llm.py`: streaming LLM wrapper
|
|
172
|
+
- `kittycode/context.py`: context compression
|
|
173
|
+
- `kittycode/cli.py`: interactive and one-shot CLI
|
|
174
|
+
- `kittycode/session.py`: session persistence
|
|
175
|
+
- `kittycode/tools/`: built-in tools
|
|
176
|
+
- `tests/`: focused runtime and tool tests
|
|
177
|
+
|
|
178
|
+
## Development
|
|
179
|
+
|
|
180
|
+
Run the test suite:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
python -m pytest -q
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The current test suite covers the exported API, config-file behavior, provider conversion helpers, context compression, session helpers, the default tool registry, and skill discovery/prompt injection.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# KittyCode
|
|
2
|
+
|
|
3
|
+
KittyCode 是一个运行在终端里的轻量级 AI 编程代理,目标是在尽量精简代码的前提下保留清晰、直接的核心运行逻辑,包括主代理循环、本地工具调用、上下文压缩、命令行交互,以及对 OpenAI 兼容接口和 Anthropic 接口的支持。
|
|
4
|
+
|
|
5
|
+
## 项目背景
|
|
6
|
+
|
|
7
|
+
KittyCode 的基本工作方式如下:
|
|
8
|
+
|
|
9
|
+
- 用户输入先通过当前配置的模型接口发送出去。
|
|
10
|
+
- 模型可以直接回答,也可以发起工具调用。
|
|
11
|
+
- 本地执行工具后,再把结果回填给模型继续推理。
|
|
12
|
+
- 直到模型返回普通文本,当前任务才结束。
|
|
13
|
+
|
|
14
|
+
这个项目刻意保持小而清晰,只保留最核心的运行时能力,包括代理主循环、命令行界面、会话保存、上下文压缩,以及默认工具集。
|
|
15
|
+
|
|
16
|
+
## 功能特点
|
|
17
|
+
|
|
18
|
+
- 保留核心 agent loop,并支持多工具并行执行。
|
|
19
|
+
- LLM 适配层同时支持 OpenAI 兼容接口和 Anthropic 接口。
|
|
20
|
+
- 内置 Bash、读文件、写文件、精确替换编辑、Glob 搜索、Grep 搜索、子代理等工具。
|
|
21
|
+
- 启动时自动扫描 `~/.kittycode/skills`,并在每轮系统 prompt 开头注入可用 skill 列表。
|
|
22
|
+
- 支持交互式 REPL 和单次命令模式。
|
|
23
|
+
- 支持长会话上下文压缩。
|
|
24
|
+
- 支持保存和恢复历史会话。
|
|
25
|
+
|
|
26
|
+
## 环境要求
|
|
27
|
+
|
|
28
|
+
- Python 3.10 及以上
|
|
29
|
+
- OpenAI 兼容接口或 Anthropic 接口所需的 API Key
|
|
30
|
+
|
|
31
|
+
## 安装方法
|
|
32
|
+
|
|
33
|
+
进入项目目录后,以可编辑模式安装:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd KittyCode
|
|
37
|
+
python -m pip install -e .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
如果还要安装测试依赖:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python -m pip install -e .[dev]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 配置说明
|
|
47
|
+
|
|
48
|
+
KittyCode 启动时会从 `~/.kittycode/config.json` 读取配置。
|
|
49
|
+
|
|
50
|
+
支持的字段包括:
|
|
51
|
+
|
|
52
|
+
- `interface`:`openai` 或 `anthropic`
|
|
53
|
+
- `api_key`
|
|
54
|
+
- `model`
|
|
55
|
+
- `base_url`
|
|
56
|
+
- `max_tokens`
|
|
57
|
+
- `temperature`
|
|
58
|
+
- `max_context`
|
|
59
|
+
|
|
60
|
+
OpenAI 兼容接口示例:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"interface": "openai",
|
|
65
|
+
"api_key": "sk-...",
|
|
66
|
+
"model": "gpt-4o",
|
|
67
|
+
"base_url": "https://api.openai.com/v1",
|
|
68
|
+
"max_tokens": 4096,
|
|
69
|
+
"temperature": 0,
|
|
70
|
+
"max_context": 128000
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Anthropic 接口示例:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"interface": "anthropic",
|
|
79
|
+
"api_key": "sk-ant-...",
|
|
80
|
+
"model": "claude-3-7-sonnet-latest",
|
|
81
|
+
"base_url": "https://api.anthropic.com",
|
|
82
|
+
"max_tokens": 4096,
|
|
83
|
+
"temperature": 0,
|
|
84
|
+
"max_context": 128000
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
如果需要,也可以通过命令行参数临时覆盖这些配置,例如 `--model`、`--interface`、`--base-url`、`--api-key`。
|
|
89
|
+
|
|
90
|
+
## Skills 机制
|
|
91
|
+
|
|
92
|
+
KittyCode 启动时会扫描 `~/.kittycode/skills` 目录。每个 skill 使用一个独立文件夹,文件夹顶层需要有一个 `SKILL.md`。
|
|
93
|
+
|
|
94
|
+
目录结构示例:
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
~/.kittycode/skills/
|
|
98
|
+
example-skill/
|
|
99
|
+
SKILL.md
|
|
100
|
+
other-files...
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
运行时会从每个 `SKILL.md` 开头读取 `name` 和 `description`,并把得到的 skill 列表缓存在进程内存中。每一轮对话时,系统 prompt 最前面都会包含以下字段:
|
|
104
|
+
|
|
105
|
+
- `name`
|
|
106
|
+
- `description`
|
|
107
|
+
- `path`
|
|
108
|
+
|
|
109
|
+
这样模型就能先看到本地有哪些 skill,再按需读取对应目录下的 `SKILL.md` 和其他相关文件。
|
|
110
|
+
|
|
111
|
+
每轮对话前,KittyCode 都会检查 skill 目录是否发生变化;如果新增或修改了 skill,会自动刷新内存缓存,因此不需要重启进程。
|
|
112
|
+
|
|
113
|
+
你也可以在 CLI 中直接通过 `/<skill 名称>` 使用某个已加载的 skill。
|
|
114
|
+
|
|
115
|
+
- `/<skill 名称>`:选中该 skill,下一条普通输入会自动带上这个 skill。
|
|
116
|
+
- `/<skill 名称> <任务>`:立即用这个 skill 执行当前请求。
|
|
117
|
+
|
|
118
|
+
## 使用方法
|
|
119
|
+
|
|
120
|
+
启动交互式终端界面:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
kittycode
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
也可以直接通过模块启动:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
python -m kittycode
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
单次执行一个提示词并退出:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
kittycode -p "Explain the project structure"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
恢复历史会话:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
kittycode -r session_1234567890
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
临时覆盖模型、接口类型或接口地址:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
kittycode --interface anthropic --model claude-3-7-sonnet-latest
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## 交互命令
|
|
151
|
+
|
|
152
|
+
在 REPL 中可用的命令有:
|
|
153
|
+
|
|
154
|
+
- `/help`
|
|
155
|
+
- `/reset`
|
|
156
|
+
- `/skills`
|
|
157
|
+
- `/<skill 名称>`
|
|
158
|
+
- `/model <name>`
|
|
159
|
+
- `/tokens`
|
|
160
|
+
- `/compact`
|
|
161
|
+
- `/save`
|
|
162
|
+
- `/sessions`
|
|
163
|
+
- `/quit`
|
|
164
|
+
|
|
165
|
+
`/skills` 命令会在检测到 skill 目录变化时刷新本地缓存,并输出当前已加载的 skill 列表。
|
|
166
|
+
当输入以 `/` 开头时,CLI 还会根据前缀自动补全可用命令和 skill。
|
|
167
|
+
|
|
168
|
+
## 目录结构
|
|
169
|
+
|
|
170
|
+
- `kittycode/agent.py`:核心代理循环
|
|
171
|
+
- `kittycode/llm.py`:流式 LLM 封装
|
|
172
|
+
- `kittycode/context.py`:上下文压缩
|
|
173
|
+
- `kittycode/cli.py`:交互式与单次命令入口
|
|
174
|
+
- `kittycode/session.py`:会话持久化
|
|
175
|
+
- `kittycode/tools/`:内置工具集合
|
|
176
|
+
- `tests/`:核心运行时与工具测试
|
|
177
|
+
|
|
178
|
+
## 开发与测试
|
|
179
|
+
|
|
180
|
+
运行测试:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
python -m pytest -q
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
当前测试主要覆盖导出 API、config.json 读取、provider 转换辅助逻辑、上下文压缩、会话辅助函数、默认工具注册表,以及 skill 发现与 prompt 注入逻辑。
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""KittyCode - minimal AI coding agent."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from kittycode.agent import Agent
|
|
6
|
+
from kittycode.config import Config
|
|
7
|
+
from kittycode.llm import LLM
|
|
8
|
+
from kittycode.tools import ALL_TOOLS
|
|
9
|
+
|
|
10
|
+
__all__ = ["Agent", "Config", "LLM", "ALL_TOOLS", "__version__"]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Core agent loop.
|
|
2
|
+
|
|
3
|
+
This is the heart of KittyCode.
|
|
4
|
+
|
|
5
|
+
user message -> LLM (with tools) -> tool calls? -> execute -> loop
|
|
6
|
+
-> text reply? -> return to user
|
|
7
|
+
|
|
8
|
+
It keeps looping until the LLM responds with plain text, which means it is
|
|
9
|
+
done working and ready to report back.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import concurrent.futures
|
|
13
|
+
|
|
14
|
+
from .context import ContextManager
|
|
15
|
+
from .llm import LLM
|
|
16
|
+
from .prompt import system_prompt
|
|
17
|
+
from .skills import load_skills
|
|
18
|
+
from .tools import ALL_TOOLS, get_tool
|
|
19
|
+
from .tools.agent import AgentTool
|
|
20
|
+
from .tools.base import Tool
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Agent:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
llm: LLM,
|
|
27
|
+
tools: list[Tool] | None = None,
|
|
28
|
+
max_context_tokens: int = 128_000,
|
|
29
|
+
max_rounds: int = 50,
|
|
30
|
+
):
|
|
31
|
+
self.llm = llm
|
|
32
|
+
self.tools = tools if tools is not None else ALL_TOOLS
|
|
33
|
+
self.messages: list[dict] = []
|
|
34
|
+
self.context = ContextManager(max_tokens=max_context_tokens)
|
|
35
|
+
self.max_rounds = max_rounds
|
|
36
|
+
self.skills = []
|
|
37
|
+
self._system = ""
|
|
38
|
+
self.refresh_skills(force_reload=True)
|
|
39
|
+
|
|
40
|
+
for tool in self.tools:
|
|
41
|
+
if isinstance(tool, AgentTool):
|
|
42
|
+
tool._parent_agent = self
|
|
43
|
+
|
|
44
|
+
def _full_messages(self) -> list[dict]:
|
|
45
|
+
self.refresh_skills()
|
|
46
|
+
return [{"role": "system", "content": self._system}] + self.messages
|
|
47
|
+
|
|
48
|
+
def _tool_schemas(self) -> list[dict]:
|
|
49
|
+
return [tool.schema() for tool in self.tools]
|
|
50
|
+
|
|
51
|
+
def chat(self, user_input: str, on_token=None, on_tool=None) -> str:
|
|
52
|
+
"""Process one user message. May involve multiple LLM/tool rounds."""
|
|
53
|
+
self.messages.append({"role": "user", "content": user_input})
|
|
54
|
+
self.context.maybe_compress(self.messages, self.llm)
|
|
55
|
+
|
|
56
|
+
for _ in range(self.max_rounds):
|
|
57
|
+
response = self.llm.chat(
|
|
58
|
+
messages=self._full_messages(),
|
|
59
|
+
tools=self._tool_schemas(),
|
|
60
|
+
on_token=on_token,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if not response.tool_calls:
|
|
64
|
+
self.messages.append(response.message)
|
|
65
|
+
return response.content
|
|
66
|
+
|
|
67
|
+
self.messages.append(response.message)
|
|
68
|
+
|
|
69
|
+
if len(response.tool_calls) == 1:
|
|
70
|
+
tool_call = response.tool_calls[0]
|
|
71
|
+
if on_tool:
|
|
72
|
+
on_tool(tool_call.name, tool_call.arguments)
|
|
73
|
+
result = self._exec_tool(tool_call)
|
|
74
|
+
self.messages.append(
|
|
75
|
+
{
|
|
76
|
+
"role": "tool",
|
|
77
|
+
"tool_call_id": tool_call.id,
|
|
78
|
+
"content": result,
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
results = self._exec_tools_parallel(response.tool_calls, on_tool)
|
|
83
|
+
for tool_call, result in zip(response.tool_calls, results):
|
|
84
|
+
self.messages.append(
|
|
85
|
+
{
|
|
86
|
+
"role": "tool",
|
|
87
|
+
"tool_call_id": tool_call.id,
|
|
88
|
+
"content": result,
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self.context.maybe_compress(self.messages, self.llm)
|
|
93
|
+
|
|
94
|
+
return "(reached maximum tool-call rounds)"
|
|
95
|
+
|
|
96
|
+
def _exec_tool(self, tool_call) -> str:
|
|
97
|
+
"""Execute a single tool call and return the result string."""
|
|
98
|
+
tool = get_tool(tool_call.name)
|
|
99
|
+
if tool is None:
|
|
100
|
+
return f"Error: unknown tool '{tool_call.name}'"
|
|
101
|
+
try:
|
|
102
|
+
return tool.execute(**tool_call.arguments)
|
|
103
|
+
except TypeError as exc:
|
|
104
|
+
return f"Error: bad arguments for {tool_call.name}: {exc}"
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
return f"Error executing {tool_call.name}: {exc}"
|
|
107
|
+
|
|
108
|
+
def _exec_tools_parallel(self, tool_calls, on_tool=None) -> list[str]:
|
|
109
|
+
"""Run multiple tool calls concurrently using threads."""
|
|
110
|
+
for tool_call in tool_calls:
|
|
111
|
+
if on_tool:
|
|
112
|
+
on_tool(tool_call.name, tool_call.arguments)
|
|
113
|
+
|
|
114
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool:
|
|
115
|
+
futures = [pool.submit(self._exec_tool, tool_call) for tool_call in tool_calls]
|
|
116
|
+
return [future.result() for future in futures]
|
|
117
|
+
|
|
118
|
+
def reset(self):
|
|
119
|
+
"""Clear conversation history."""
|
|
120
|
+
self.messages.clear()
|
|
121
|
+
|
|
122
|
+
def refresh_skills(self, force_reload: bool = False):
|
|
123
|
+
"""Refresh cached skill metadata and rebuild the system prompt."""
|
|
124
|
+
self.skills = load_skills(force_reload=force_reload)
|
|
125
|
+
self._system = system_prompt(self.tools, self.skills)
|