tea-agent 0.2.4__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.
- tea_agent-0.2.4/PKG-INFO +87 -0
- tea_agent-0.2.4/README.md +71 -0
- tea_agent-0.2.4/pyproject.toml +39 -0
- tea_agent-0.2.4/setup.cfg +4 -0
- tea_agent-0.2.4/tea_agent/__init__.py +1 -0
- tea_agent-0.2.4/tea_agent/main_db_gui.py +482 -0
- tea_agent-0.2.4/tea_agent/memory.py +310 -0
- tea_agent-0.2.4/tea_agent/onlinesession.py +774 -0
- tea_agent-0.2.4/tea_agent/store.py +143 -0
- tea_agent-0.2.4/tea_agent/tlk.py +386 -0
- tea_agent-0.2.4/tea_agent/toolkit/__init__.py +1 -0
- tea_agent-0.2.4/tea_agent/toolkit/toolkit_dir.py +9 -0
- tea_agent-0.2.4/tea_agent/toolkit/toolkit_evolve_comment.py +20 -0
- tea_agent-0.2.4/tea_agent/toolkit/toolkit_exec.py +10 -0
- tea_agent-0.2.4/tea_agent/toolkit/toolkit_fix_pyproject.py +73 -0
- tea_agent-0.2.4/tea_agent/toolkit/toolkit_gettime.py +16 -0
- tea_agent-0.2.4/tea_agent/toolkit/toolkit_list_dir.py +57 -0
- tea_agent-0.2.4/tea_agent/toolkit/toolkit_load_file.py +14 -0
- tea_agent-0.2.4/tea_agent/toolkit/toolkit_memory_extraction_strategy.py +62 -0
- tea_agent-0.2.4/tea_agent/toolkit/toolkit_save_file.py +13 -0
- tea_agent-0.2.4/tea_agent/toolkit/toolkit_self_report.py +92 -0
- tea_agent-0.2.4/tea_agent.egg-info/PKG-INFO +87 -0
- tea_agent-0.2.4/tea_agent.egg-info/SOURCES.txt +25 -0
- tea_agent-0.2.4/tea_agent.egg-info/dependency_links.txt +1 -0
- tea_agent-0.2.4/tea_agent.egg-info/entry_points.txt +2 -0
- tea_agent-0.2.4/tea_agent.egg-info/requires.txt +3 -0
- tea_agent-0.2.4/tea_agent.egg-info/top_level.txt +1 -0
tea_agent-0.2.4/PKG-INFO
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tea_agent
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: A self-evolving AI agent with dynamic toolkit management
|
|
5
|
+
Author-email: sunkw <sunkwei@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sunkwei/tea_agent
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: openai>=1.0.0
|
|
14
|
+
Requires-Dist: markdown>=3.4.0
|
|
15
|
+
Requires-Dist: tkinterweb>=3.10
|
|
16
|
+
|
|
17
|
+
# TeaAgent
|
|
18
|
+
|
|
19
|
+
TeaAgent 是一个**自主进化型智能助手**,基于 OpenAI 兼容接口的 Function Calling 功能实现。它不仅能够调用预设的工具,还具备动态创建、加载和管理工具的能力,实现能力的自我扩展。
|
|
20
|
+
|
|
21
|
+
仅仅依赖 python 的 tk 库,非常小巧,当然界面上比不过那些基于浏览器的大家伙 :)
|
|
22
|
+
|
|
23
|
+
## 警告
|
|
24
|
+
本项目未作安全沙盒,建议在虚拟机中执行,起始也不会闯什么大祸,当然你需要明白在做啥!!!
|
|
25
|
+
|
|
26
|
+
## 核心特性
|
|
27
|
+
|
|
28
|
+
- **自主进化 (Self-Evolution)**: 智能体可以根据任务需求,自动编写 Python 代码并调用 `toolkit_save` 创建新工具,随后通过 `toolkit_reload` 立即获得新能力。
|
|
29
|
+
- **动态工具库 (Dynamic Toolkit)**: 支持工具的热加载与卸载,所有工具均以独立的 Python 文件形式存储在 `toolkit` 目录中。
|
|
30
|
+
- **长期记忆 (Long-term Memory)**: 集成了 LLM 驱动的记忆提取机制,能从对话中自动识别并提取用户偏好、技术决策、事实等有价值的信息,并进行持久化存储。
|
|
31
|
+
- **流式对话与思考过程**: 支持流式输出,并可选展示模型的思考过程(Thinking Process)。
|
|
32
|
+
- **GUI 交互界面**: 提供基于 Tkinter 的图形界面,支持多主题管理、历史记录查询及工具状态实时监控。
|
|
33
|
+
- **持久化存储 (Persistent Storage)**: 所有对话、记忆及主题均保存在 SQLite 数据库中,支持历史记录查询, 数据库存储在 $HOME/.tea_agent/ 下,自动创建的工具保存在 `toolkit` 目录中。
|
|
34
|
+
|
|
35
|
+
## 快速开始
|
|
36
|
+
|
|
37
|
+
### 环境要求
|
|
38
|
+
- Python 3.10+
|
|
39
|
+
- OpenAI 兼容的 API 密钥(如 Qwen, GLM 等)
|
|
40
|
+
|
|
41
|
+
### 安装依赖
|
|
42
|
+
```bash
|
|
43
|
+
pip install -e .
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 运行
|
|
47
|
+
```bash
|
|
48
|
+
export TEA_AGENT_API=<YOUR API KEY>
|
|
49
|
+
export TEA_AGENT_URL=<YOUR API URL>
|
|
50
|
+
export TEA_AGENT_MODEL=<YOUR MODEL NAME>
|
|
51
|
+
python -m tea_agent.main_db_gui main
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 项目结构
|
|
55
|
+
- `tea_agent/`: 核心包目录
|
|
56
|
+
- `onlinesession.py`: 处理 LLM 对话流、工具调用循环及流式输出。
|
|
57
|
+
- `memory.py`: 记忆提取与管理逻辑。
|
|
58
|
+
- `store.py`: 基于 SQLite 的持久化存储(对话历史、记忆、主题)。
|
|
59
|
+
- `tlk.py`: 工具库 (Toolkit) 的加载、校验与保存逻辑。
|
|
60
|
+
- `toolkit/`: 存放动态生成的工具函数 (.py 文件)。
|
|
61
|
+
- `main_db_gui.py`: 基于 Tkinter 的 GUI 实现。
|
|
62
|
+
|
|
63
|
+
## 配置
|
|
64
|
+
三个环境变量
|
|
65
|
+
|
|
66
|
+
- TEA_AGENT_API=YOUR API KEY
|
|
67
|
+
- TEA_AGENT_URL=YOUR API URL
|
|
68
|
+
- TEA_AGENT_MODEL=YOUR MODEL NAME
|
|
69
|
+
|
|
70
|
+
## 使用示例:
|
|
71
|
+
|
|
72
|
+
1. 去年12月26号到今天过去多少天了?
|
|
73
|
+
一般来说,会自动创建一个工具函数用于获取当前时间,然后在计算间隔的天数
|
|
74
|
+
|
|
75
|
+
2. 创建一个 powershell 脚本,获取我的公网 ip,然后将该 ip 地址通过邮件发送到 sunkwei@gmail.com,测试成功后,将脚本加到windows计划任务,每天执行一次
|
|
76
|
+
需要提供你的 smtp,然后大模型都能轻松搞定
|
|
77
|
+
|
|
78
|
+
3. 字体太小了, 将输入框和 html render 窗口字体改为 14 号, 并且将字体替换为开源字体, 支持无衬中文. 然后巴拉巴拉就搞定了, 重启, 就能看到效果了.
|
|
79
|
+
|
|
80
|
+
4. 好的, 修改 pyproject.toml 将版本号修改为 0.2.3, 然后根据今天修改的内容, 先更新到 CHANGELOG.md 中, 打包测试, 如果成功, 生成一次 git 提交并 push 到远程仓库
|
|
81
|
+
|
|
82
|
+
5. 记住,每次修改代码时, 在代码的修改位置增加一条注释, 格式为: "@{date} generated by {model name}, {简单描述}", {date} 通过获取当前系统时间得到, {model name} 为 class OnlineToolSession 使用的模型, 一般通过环境变量 TEA_AGENT_MODEL 指定, {简单描述} 说明修改的目的和内容
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
## 开源协议
|
|
87
|
+
MIT License
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# TeaAgent
|
|
2
|
+
|
|
3
|
+
TeaAgent 是一个**自主进化型智能助手**,基于 OpenAI 兼容接口的 Function Calling 功能实现。它不仅能够调用预设的工具,还具备动态创建、加载和管理工具的能力,实现能力的自我扩展。
|
|
4
|
+
|
|
5
|
+
仅仅依赖 python 的 tk 库,非常小巧,当然界面上比不过那些基于浏览器的大家伙 :)
|
|
6
|
+
|
|
7
|
+
## 警告
|
|
8
|
+
本项目未作安全沙盒,建议在虚拟机中执行,起始也不会闯什么大祸,当然你需要明白在做啥!!!
|
|
9
|
+
|
|
10
|
+
## 核心特性
|
|
11
|
+
|
|
12
|
+
- **自主进化 (Self-Evolution)**: 智能体可以根据任务需求,自动编写 Python 代码并调用 `toolkit_save` 创建新工具,随后通过 `toolkit_reload` 立即获得新能力。
|
|
13
|
+
- **动态工具库 (Dynamic Toolkit)**: 支持工具的热加载与卸载,所有工具均以独立的 Python 文件形式存储在 `toolkit` 目录中。
|
|
14
|
+
- **长期记忆 (Long-term Memory)**: 集成了 LLM 驱动的记忆提取机制,能从对话中自动识别并提取用户偏好、技术决策、事实等有价值的信息,并进行持久化存储。
|
|
15
|
+
- **流式对话与思考过程**: 支持流式输出,并可选展示模型的思考过程(Thinking Process)。
|
|
16
|
+
- **GUI 交互界面**: 提供基于 Tkinter 的图形界面,支持多主题管理、历史记录查询及工具状态实时监控。
|
|
17
|
+
- **持久化存储 (Persistent Storage)**: 所有对话、记忆及主题均保存在 SQLite 数据库中,支持历史记录查询, 数据库存储在 $HOME/.tea_agent/ 下,自动创建的工具保存在 `toolkit` 目录中。
|
|
18
|
+
|
|
19
|
+
## 快速开始
|
|
20
|
+
|
|
21
|
+
### 环境要求
|
|
22
|
+
- Python 3.10+
|
|
23
|
+
- OpenAI 兼容的 API 密钥(如 Qwen, GLM 等)
|
|
24
|
+
|
|
25
|
+
### 安装依赖
|
|
26
|
+
```bash
|
|
27
|
+
pip install -e .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 运行
|
|
31
|
+
```bash
|
|
32
|
+
export TEA_AGENT_API=<YOUR API KEY>
|
|
33
|
+
export TEA_AGENT_URL=<YOUR API URL>
|
|
34
|
+
export TEA_AGENT_MODEL=<YOUR MODEL NAME>
|
|
35
|
+
python -m tea_agent.main_db_gui main
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 项目结构
|
|
39
|
+
- `tea_agent/`: 核心包目录
|
|
40
|
+
- `onlinesession.py`: 处理 LLM 对话流、工具调用循环及流式输出。
|
|
41
|
+
- `memory.py`: 记忆提取与管理逻辑。
|
|
42
|
+
- `store.py`: 基于 SQLite 的持久化存储(对话历史、记忆、主题)。
|
|
43
|
+
- `tlk.py`: 工具库 (Toolkit) 的加载、校验与保存逻辑。
|
|
44
|
+
- `toolkit/`: 存放动态生成的工具函数 (.py 文件)。
|
|
45
|
+
- `main_db_gui.py`: 基于 Tkinter 的 GUI 实现。
|
|
46
|
+
|
|
47
|
+
## 配置
|
|
48
|
+
三个环境变量
|
|
49
|
+
|
|
50
|
+
- TEA_AGENT_API=YOUR API KEY
|
|
51
|
+
- TEA_AGENT_URL=YOUR API URL
|
|
52
|
+
- TEA_AGENT_MODEL=YOUR MODEL NAME
|
|
53
|
+
|
|
54
|
+
## 使用示例:
|
|
55
|
+
|
|
56
|
+
1. 去年12月26号到今天过去多少天了?
|
|
57
|
+
一般来说,会自动创建一个工具函数用于获取当前时间,然后在计算间隔的天数
|
|
58
|
+
|
|
59
|
+
2. 创建一个 powershell 脚本,获取我的公网 ip,然后将该 ip 地址通过邮件发送到 sunkwei@gmail.com,测试成功后,将脚本加到windows计划任务,每天执行一次
|
|
60
|
+
需要提供你的 smtp,然后大模型都能轻松搞定
|
|
61
|
+
|
|
62
|
+
3. 字体太小了, 将输入框和 html render 窗口字体改为 14 号, 并且将字体替换为开源字体, 支持无衬中文. 然后巴拉巴拉就搞定了, 重启, 就能看到效果了.
|
|
63
|
+
|
|
64
|
+
4. 好的, 修改 pyproject.toml 将版本号修改为 0.2.3, 然后根据今天修改的内容, 先更新到 CHANGELOG.md 中, 打包测试, 如果成功, 生成一次 git 提交并 push 到远程仓库
|
|
65
|
+
|
|
66
|
+
5. 记住,每次修改代码时, 在代码的修改位置增加一条注释, 格式为: "@{date} generated by {model name}, {简单描述}", {date} 通过获取当前系统时间得到, {model name} 为 class OnlineToolSession 使用的模型, 一般通过环境变量 TEA_AGENT_MODEL 指定, {简单描述} 说明修改的目的和内容
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
## 开源协议
|
|
71
|
+
MIT License
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tea_agent"
|
|
7
|
+
version = "0.2.4"
|
|
8
|
+
description = "A self-evolving AI agent with dynamic toolkit management"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "sunkw", email = "sunkwei@gmail.com" }
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
dependencies = [
|
|
23
|
+
"openai>=1.0.0",
|
|
24
|
+
"markdown>=3.4.0",
|
|
25
|
+
"tkinterweb>=3.10",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/sunkwei/tea_agent"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
tea_agent = "tea_agent.main_db_gui:main"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
include-package-data = true
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["."]
|
|
39
|
+
include = ["tea_agent*"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from tkinter import ttk, scrolledtext, Listbox, Frame
|
|
3
|
+
import threading
|
|
4
|
+
import os
|
|
5
|
+
import os.path as osp
|
|
6
|
+
import sys
|
|
7
|
+
import re
|
|
8
|
+
import html as html_mod
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Dict, cast, Callable, Optional, List, Tuple
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from tkinterweb import HtmlFrame
|
|
15
|
+
import markdown
|
|
16
|
+
HAS_TKINTERWEB = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
HAS_TKINTERWEB = False
|
|
19
|
+
|
|
20
|
+
# ====================== 包导入兼容处理 ======================
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
parent_dir = str(Path(__file__).resolve().parent.parent)
|
|
23
|
+
if parent_dir not in sys.path:
|
|
24
|
+
sys.path.insert(0, parent_dir)
|
|
25
|
+
from tea_agent.onlinesession import OnlineToolSession
|
|
26
|
+
from tea_agent.store import Storage
|
|
27
|
+
from tea_agent.memory import Memory, get_memory
|
|
28
|
+
from tea_agent import tlk
|
|
29
|
+
else:
|
|
30
|
+
from .onlinesession import OnlineToolSession
|
|
31
|
+
from .store import Storage
|
|
32
|
+
from .memory import Memory, get_memory
|
|
33
|
+
from . import tlk
|
|
34
|
+
|
|
35
|
+
# ====================== 配置区 ======================
|
|
36
|
+
API_KEY = os.environ.get("TEA_AGENT_KEY")
|
|
37
|
+
API_URL = os.environ.get("TEA_AGENT_URL")
|
|
38
|
+
MODEL = os.environ.get("TEA_AGENT_MODEL")
|
|
39
|
+
|
|
40
|
+
if not API_KEY or not API_URL or not MODEL:
|
|
41
|
+
print("错误: 请设置以下环境变量:")
|
|
42
|
+
print(" TEA_AGENT_KEY : API 密钥")
|
|
43
|
+
print(" TEA_AGENT_URL : API 地址")
|
|
44
|
+
print(" TEA_AGENT_MODEL : 模型名称")
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
_storage_ = None
|
|
48
|
+
_toolkit_ = None
|
|
49
|
+
_memory_ = None
|
|
50
|
+
|
|
51
|
+
# ====================== Markdown → HTML 渲染 ======================
|
|
52
|
+
|
|
53
|
+
_MD_CSS = """
|
|
54
|
+
<style>
|
|
55
|
+
body { font-family: "Noto Sans CJK SC", "Source Han Sans SC", "WenQuanYi Micro Hei", "WenQuanYi Zen Hei", sans-serif; font-size: 16px; line-height: 1.6; color: #333; padding: 8px; }
|
|
56
|
+
h1, h2, h3, h4, h5, h6 { margin: 0.8em 0 0.4em; color: #1a73e8; }
|
|
57
|
+
h1 { font-size: 1.5em; border-bottom: 2px solid #eee; padding-bottom: 0.3em; }
|
|
58
|
+
h2 { font-size: 1.3em; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
|
|
59
|
+
p { margin: 0.5em 0; }
|
|
60
|
+
code { background: #f4f4f4; padding: 2px 5px; border-radius: 3px; font-family: "Noto Sans Mono CJK SC", "Source Han Mono SC", "WenQuanYi Micro Hei Mono", "Consolas", "Courier New", monospace; font-size: 0.9em; }
|
|
61
|
+
pre { background: #f6f8fa; border: 1px solid #ddd; border-radius: 5px; padding: 12px; overflow-x: auto; }
|
|
62
|
+
pre code { background: none; padding: 0; }
|
|
63
|
+
ul, ol { padding-left: 1.5em; }
|
|
64
|
+
li { margin: 0.3em 0; }
|
|
65
|
+
blockquote { border-left: 4px solid #ddd; margin: 0.5em 0; padding: 0.5em 1em; color: #666; background: #f9f9f9; }
|
|
66
|
+
table { border-collapse: collapse; width: 100%; margin: 0.8em 0; }
|
|
67
|
+
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
|
68
|
+
th { background: #f2f2f2; font-weight: bold; }
|
|
69
|
+
a { color: #1a73e8; text-decoration: none; }
|
|
70
|
+
a:hover { text-decoration: underline; }
|
|
71
|
+
hr { border: none; border-top: 1px solid #ddd; margin: 1em 0; }
|
|
72
|
+
strong { font-weight: bold; color: #222; }
|
|
73
|
+
em { font-style: italic; }
|
|
74
|
+
.msg-timestamp { font-size: 0.8em; color: #999; margin-bottom: 0.3em; }
|
|
75
|
+
.msg-divider { border: none; border-top: 2px solid #e8e8e8; margin: 1.2em 0; }
|
|
76
|
+
</style>
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _render_markdown(text: str) -> str:
|
|
81
|
+
"""将 markdown 文本转换为带样式的 HTML 片段"""
|
|
82
|
+
if not HAS_TKINTERWEB:
|
|
83
|
+
return text
|
|
84
|
+
html_body = markdown.markdown(text, extensions=["fenced_code", "tables", "codehilite"])
|
|
85
|
+
return f"<html><head>{_MD_CSS}</head><body>{html_body}</body></html>"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _chat_to_markdown(messages: List[Dict]) -> str:
|
|
89
|
+
"""将聊天消息列表转换为 markdown 格式,包含时间戳和分割线"""
|
|
90
|
+
parts = []
|
|
91
|
+
for msg in messages:
|
|
92
|
+
role = msg.get("role", "")
|
|
93
|
+
content = msg.get("content", "")
|
|
94
|
+
ts = msg.get("timestamp", "")
|
|
95
|
+
ts_display = f"<span class=\"msg-timestamp\">{ts}</span>" if ts else ""
|
|
96
|
+
if role == "user":
|
|
97
|
+
parts.append(f"{ts_display}\n\n### 👤 你\n\n{content.strip()}\n")
|
|
98
|
+
elif role == "ai":
|
|
99
|
+
parts.append(f"{ts_display}\n\n### 🤖 AI\n\n{content.strip()}\n\n---\n")
|
|
100
|
+
elif role == "tool":
|
|
101
|
+
parts.append(f"{ts_display}\n> 🔧 **工具**: {content.strip()}\n")
|
|
102
|
+
elif role == "notice":
|
|
103
|
+
parts.append(f"\n---\n*{content.strip()}*\n---\n")
|
|
104
|
+
return "\n".join(parts)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ====================== GUI 主界面 ======================
|
|
108
|
+
class TkGUI:
|
|
109
|
+
def __init__(self, root):
|
|
110
|
+
self.root = root
|
|
111
|
+
self.root.title("AI 工具调用助手")
|
|
112
|
+
self.root.geometry("1100x750")
|
|
113
|
+
self.root.minsize(900, 600)
|
|
114
|
+
|
|
115
|
+
root_path = Path.home() / ".tea_agent"
|
|
116
|
+
if not root_path.exists():
|
|
117
|
+
os.makedirs(root_path, exist_ok=True)
|
|
118
|
+
|
|
119
|
+
db_path = root_path / "chat_history.db"
|
|
120
|
+
tool_dir = root_path / "toolkit"
|
|
121
|
+
if not tool_dir.exists():
|
|
122
|
+
os.makedirs(tool_dir, exist_ok=True)
|
|
123
|
+
|
|
124
|
+
self.db = Storage(db_path=str(db_path))
|
|
125
|
+
self.toolkit = tlk.Toolkit(str(tool_dir))
|
|
126
|
+
|
|
127
|
+
# 初始化 Memory
|
|
128
|
+
self.memory = get_memory()
|
|
129
|
+
|
|
130
|
+
globals()["_storage_"] = self.db
|
|
131
|
+
globals()["_memory_"] = self.memory
|
|
132
|
+
globals()["tlk"]._toolkit_ = self.toolkit
|
|
133
|
+
|
|
134
|
+
tlk.toolkit_reload()
|
|
135
|
+
|
|
136
|
+
# 会话相关
|
|
137
|
+
self.current_topic_id = -1
|
|
138
|
+
self.generating = False
|
|
139
|
+
|
|
140
|
+
# Thinking 开关状态
|
|
141
|
+
self.enable_thinking_var = tk.BooleanVar(value=True)
|
|
142
|
+
|
|
143
|
+
# 聊天消息列表 — 用于最终渲染
|
|
144
|
+
# 格式: [{"role": "user"|"ai"|"tool"|"notice", "content": "...", "timestamp": "..."}, ...]
|
|
145
|
+
self.chat_messages: List[Dict] = []
|
|
146
|
+
|
|
147
|
+
# 当前 stream 累积 buffer
|
|
148
|
+
self._stream_buffer = ""
|
|
149
|
+
|
|
150
|
+
# 创建界面
|
|
151
|
+
self._create_ui()
|
|
152
|
+
|
|
153
|
+
# 初始化会话
|
|
154
|
+
self._init_session()
|
|
155
|
+
|
|
156
|
+
# 加载主题
|
|
157
|
+
self.refresh_topics()
|
|
158
|
+
self.auto_new_topic()
|
|
159
|
+
self.show_tool_list()
|
|
160
|
+
|
|
161
|
+
def _create_ui(self):
|
|
162
|
+
"""创建界面"""
|
|
163
|
+
main_split = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
|
|
164
|
+
main_split.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)
|
|
165
|
+
|
|
166
|
+
# ===== 左侧面板 =====
|
|
167
|
+
left = Frame(main_split, width=220)
|
|
168
|
+
main_split.add(left, weight=1)
|
|
169
|
+
|
|
170
|
+
ttk.Label(left, text="聊天主题", font=("Noto Sans CJK SC", 12, "bold")).pack(pady=5)
|
|
171
|
+
self.topic_list = Listbox(left, font=("Noto Sans CJK SC", 10))
|
|
172
|
+
self.topic_list.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
|
|
173
|
+
self.topic_list.bind("<<ListboxSelect>>", self.on_topic_select)
|
|
174
|
+
ttk.Button(left, text="➕ 新建主题", command=self.new_topic).pack(
|
|
175
|
+
fill=tk.X, padx=4, pady=2)
|
|
176
|
+
|
|
177
|
+
# ===== 右侧面板 =====
|
|
178
|
+
right = Frame(main_split)
|
|
179
|
+
main_split.add(right, weight=5)
|
|
180
|
+
|
|
181
|
+
# 状态栏
|
|
182
|
+
self.status_var = tk.StringVar(value="就绪")
|
|
183
|
+
ttk.Label(right, textvariable=self.status_var,
|
|
184
|
+
foreground="#666").pack(anchor=tk.E, padx=6)
|
|
185
|
+
|
|
186
|
+
# 聊天区域
|
|
187
|
+
chat_split = ttk.PanedWindow(right, orient=tk.VERTICAL)
|
|
188
|
+
chat_split.pack(fill=tk.BOTH, expand=True)
|
|
189
|
+
|
|
190
|
+
chat_frame = Frame(chat_split)
|
|
191
|
+
chat_split.add(chat_frame, weight=4)
|
|
192
|
+
|
|
193
|
+
# --- 组件 1: console (ScrolledText) — 用于显示中间结果 ---
|
|
194
|
+
self.console = scrolledtext.ScrolledText(
|
|
195
|
+
chat_frame, font=("Noto Sans CJK SC", 11), bg="white", fg="black", wrap=tk.WORD
|
|
196
|
+
)
|
|
197
|
+
self.console.config(state=tk.DISABLED)
|
|
198
|
+
|
|
199
|
+
# --- 组件 2: chat_view (HtmlFrame) — 用于显示最终聊天信息 ---
|
|
200
|
+
if HAS_TKINTERWEB:
|
|
201
|
+
self.chat_view = HtmlFrame(chat_frame, messages_enabled=False)
|
|
202
|
+
else:
|
|
203
|
+
self.chat_view = scrolledtext.ScrolledText(
|
|
204
|
+
chat_frame, font=("Noto Sans CJK SC", 11), bg="#fafafa", fg="black", wrap=tk.WORD
|
|
205
|
+
)
|
|
206
|
+
self.chat_view.config(state=tk.DISABLED)
|
|
207
|
+
|
|
208
|
+
# 默认显示 console
|
|
209
|
+
self._show_mode = "console"
|
|
210
|
+
self._switch_display("console")
|
|
211
|
+
|
|
212
|
+
# 输入区域
|
|
213
|
+
input_frame = Frame(chat_split)
|
|
214
|
+
chat_split.add(input_frame, weight=1)
|
|
215
|
+
self.input_box = scrolledtext.ScrolledText(
|
|
216
|
+
input_frame, font=("Noto Sans CJK SC", 14), height=4, bg="#f8f8f8"
|
|
217
|
+
)
|
|
218
|
+
self.input_box.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
|
|
219
|
+
|
|
220
|
+
self.thinking_check = ttk.Checkbutton(
|
|
221
|
+
input_frame,
|
|
222
|
+
text="🧠 启用 Thinking",
|
|
223
|
+
variable=self.enable_thinking_var,
|
|
224
|
+
command=self.on_thinking_toggle
|
|
225
|
+
)
|
|
226
|
+
self.thinking_check.pack(anchor=tk.W, padx=6, pady=2)
|
|
227
|
+
|
|
228
|
+
ttk.Label(input_frame, text="Enter 发送 | Shift+Enter 换行 | Ctrl+C 打断",
|
|
229
|
+
foreground="#666").pack(anchor=tk.E, padx=6)
|
|
230
|
+
|
|
231
|
+
# 样式配置
|
|
232
|
+
self.console.tag_configure("user", foreground="#0055cc")
|
|
233
|
+
self.console.tag_configure("ai", foreground="black")
|
|
234
|
+
self.console.tag_configure("tool", foreground="#d68000")
|
|
235
|
+
self.console.tag_configure(
|
|
236
|
+
"title", foreground="#0066cc", font=("Noto Sans CJK SC", 12, "bold"))
|
|
237
|
+
self.console.tag_configure("notice", foreground="#008800")
|
|
238
|
+
self.console.tag_configure("error", foreground="#cc0000")
|
|
239
|
+
|
|
240
|
+
# 快捷键绑定
|
|
241
|
+
self.input_box.bind("<Return>", self.send)
|
|
242
|
+
self.input_box.bind("<Shift-Return>", self.newline)
|
|
243
|
+
self.root.bind("<Control-c>", self.interrupt)
|
|
244
|
+
|
|
245
|
+
def _switch_display(self, mode: str):
|
|
246
|
+
"""切换显示模式: 'console' 或 'chat_view'"""
|
|
247
|
+
if mode == self._show_mode:
|
|
248
|
+
return
|
|
249
|
+
self._show_mode = mode
|
|
250
|
+
if mode == "console":
|
|
251
|
+
self.chat_view.pack_forget()
|
|
252
|
+
self.console.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
|
|
253
|
+
else:
|
|
254
|
+
self.console.pack_forget()
|
|
255
|
+
self.chat_view.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
|
|
256
|
+
self.root.after(400, self.scroll_to_bottom) # 延迟一点,等渲染完成
|
|
257
|
+
|
|
258
|
+
def scroll_to_bottom(self):
|
|
259
|
+
# HtmlFrame 内部通常有一个 canvas 或 text,可以通过属性访问并滚动
|
|
260
|
+
self.chat_view.yview_moveto(1.0)
|
|
261
|
+
|
|
262
|
+
def _render_chat(self):
|
|
263
|
+
"""将 self.chat_messages 渲染到 chat_view"""
|
|
264
|
+
md = _chat_to_markdown(self.chat_messages)
|
|
265
|
+
if HAS_TKINTERWEB:
|
|
266
|
+
html = _render_markdown(md)
|
|
267
|
+
self.chat_view.load_html(html)
|
|
268
|
+
else:
|
|
269
|
+
self.chat_view.config(state=tk.NORMAL)
|
|
270
|
+
self.chat_view.delete("1.0", tk.END)
|
|
271
|
+
self.chat_view.insert("1.0", md)
|
|
272
|
+
self.chat_view.config(state=tk.DISABLED)
|
|
273
|
+
self.chat_view.see(tk.END)
|
|
274
|
+
|
|
275
|
+
def _now_ts(self) -> str:
|
|
276
|
+
"""获取当前时间戳字符串"""
|
|
277
|
+
return datetime.now().strftime("%H:%M:%S")
|
|
278
|
+
|
|
279
|
+
def _init_session(self):
|
|
280
|
+
"""初始化会话"""
|
|
281
|
+
self.sess = OnlineToolSession(
|
|
282
|
+
toolkit=self.toolkit,
|
|
283
|
+
api_key=API_KEY,
|
|
284
|
+
api_url=API_URL,
|
|
285
|
+
model=MODEL,
|
|
286
|
+
max_history=10,
|
|
287
|
+
memory=self.memory,
|
|
288
|
+
storage=self.db,
|
|
289
|
+
)
|
|
290
|
+
self.sess.enable_thinking = self.enable_thinking_var.get()
|
|
291
|
+
|
|
292
|
+
self.sess.tool_log = self.safe_log_tool
|
|
293
|
+
self._update_status(f"📡 已连接 | 模型: {MODEL} | 💾 Memory 已启用")
|
|
294
|
+
|
|
295
|
+
def _update_status(self, msg: str):
|
|
296
|
+
"""更新状态栏"""
|
|
297
|
+
self.status_var.set(msg)
|
|
298
|
+
|
|
299
|
+
# ====================== 安全 UI 更新 ======================
|
|
300
|
+
def safe_stream(self, text):
|
|
301
|
+
self.root.after(0, self.stream, text)
|
|
302
|
+
|
|
303
|
+
def safe_log(self, msg, tag="ai"):
|
|
304
|
+
self.root.after(0, self.log, msg, tag)
|
|
305
|
+
|
|
306
|
+
def safe_log_tool(self, msg: str):
|
|
307
|
+
self.root.after(0, self.log_tool, msg)
|
|
308
|
+
|
|
309
|
+
def on_thinking_toggle(self):
|
|
310
|
+
"""Thinking 开关切换回调"""
|
|
311
|
+
if hasattr(self, 'sess') and self.sess:
|
|
312
|
+
self.sess.enable_thinking = self.enable_thinking_var.get()
|
|
313
|
+
state = "已开启" if self.enable_thinking_var.get() else "已关闭"
|
|
314
|
+
self._update_status(f"🧠 Thinking {state}")
|
|
315
|
+
|
|
316
|
+
def show_tool_list(self):
|
|
317
|
+
self.log("=" * 50, "title")
|
|
318
|
+
self.log(f"📦 已加载工具函数(共 {len(self.toolkit.func_map)} 个)", "title")
|
|
319
|
+
for name in self.toolkit.func_map.keys():
|
|
320
|
+
self.log(f"✅ {name}", "notice")
|
|
321
|
+
self.log("=" * 50, "title")
|
|
322
|
+
|
|
323
|
+
stats = self.memory.get_stats()
|
|
324
|
+
self.log(f"💾 Memory: {stats['total']} 条记忆", "notice")
|
|
325
|
+
self.log("")
|
|
326
|
+
|
|
327
|
+
def log(self, msg, tag="ai"):
|
|
328
|
+
"""向 console 追加一行文本,同时记录到 chat_messages"""
|
|
329
|
+
self.console.config(state=tk.NORMAL)
|
|
330
|
+
self.console.insert(tk.END, msg + "\n", tag)
|
|
331
|
+
self.console.see(tk.END)
|
|
332
|
+
self.console.config(state=tk.DISABLED)
|
|
333
|
+
|
|
334
|
+
if tag in ("user", "ai", "tool", "notice"):
|
|
335
|
+
self.chat_messages.append({"role": tag, "content": msg, "timestamp": self._now_ts()})
|
|
336
|
+
|
|
337
|
+
def stream(self, text):
|
|
338
|
+
"""流式输出到 console,同时累积到 _stream_buffer"""
|
|
339
|
+
self.console.config(state=tk.NORMAL)
|
|
340
|
+
self.console.insert(tk.END, text)
|
|
341
|
+
self.console.see(tk.END)
|
|
342
|
+
self.console.config(state=tk.DISABLED)
|
|
343
|
+
|
|
344
|
+
self._stream_buffer += text
|
|
345
|
+
|
|
346
|
+
def log_tool(self, msg: str):
|
|
347
|
+
self.log(msg, "tool")
|
|
348
|
+
|
|
349
|
+
def _flush_stream_to_messages(self):
|
|
350
|
+
"""将当前 stream buffer 追加到 chat_messages 的 AI 消息中"""
|
|
351
|
+
if self._stream_buffer:
|
|
352
|
+
if self.chat_messages and self.chat_messages[-1]["role"] == "ai":
|
|
353
|
+
self.chat_messages[-1]["content"] += self._stream_buffer
|
|
354
|
+
else:
|
|
355
|
+
self.chat_messages.append({"role": "ai", "content": self._stream_buffer, "timestamp": self._now_ts()})
|
|
356
|
+
self._stream_buffer = ""
|
|
357
|
+
|
|
358
|
+
def clear_chat(self):
|
|
359
|
+
"""清空 console 和 chat_messages"""
|
|
360
|
+
self.console.config(state=tk.NORMAL)
|
|
361
|
+
self.console.delete("1.0", tk.END)
|
|
362
|
+
self.console.config(state=tk.DISABLED)
|
|
363
|
+
self.chat_messages.clear()
|
|
364
|
+
self._stream_buffer = ""
|
|
365
|
+
|
|
366
|
+
def auto_new_topic(self):
|
|
367
|
+
topics = self.db.list_topics()
|
|
368
|
+
if topics:
|
|
369
|
+
self.topic_list.select_set(0)
|
|
370
|
+
self.on_topic_select(None)
|
|
371
|
+
else:
|
|
372
|
+
self.new_topic()
|
|
373
|
+
|
|
374
|
+
def new_topic(self):
|
|
375
|
+
title = f"主题 {datetime.now().strftime('%m-%d %H:%M:%S')}"
|
|
376
|
+
tid = self.db.create_topic(title)
|
|
377
|
+
self.refresh_topics()
|
|
378
|
+
self.switch_topic(tid)
|
|
379
|
+
|
|
380
|
+
def refresh_topics(self):
|
|
381
|
+
self.topic_list.delete(0, tk.END)
|
|
382
|
+
for tp in self.db.list_topics():
|
|
383
|
+
self.topic_list.insert(tk.END, tp["title"])
|
|
384
|
+
|
|
385
|
+
def switch_topic(self, topic_id):
|
|
386
|
+
self.current_topic_id = topic_id
|
|
387
|
+
self.clear_chat()
|
|
388
|
+
topic = cast(dict, self.db.get_topic(topic_id))
|
|
389
|
+
self.log(f"📌 当前主题:{topic['title']}", "title")
|
|
390
|
+
self.log("-" * 50, "notice")
|
|
391
|
+
|
|
392
|
+
conversations = self.db.get_conversations(topic_id)
|
|
393
|
+
self.sess.load_history(conversations)
|
|
394
|
+
for c in conversations:
|
|
395
|
+
self.log(f"你:{c['user_msg']}", "user")
|
|
396
|
+
self.log(f"AI:{c['ai_msg']}", "ai")
|
|
397
|
+
if c["is_func_calling"]:
|
|
398
|
+
self.log("ℹ️ 本条使用了工具调用", "tool")
|
|
399
|
+
self.log("")
|
|
400
|
+
|
|
401
|
+
if HAS_TKINTERWEB and self.chat_messages:
|
|
402
|
+
self._render_chat()
|
|
403
|
+
self._switch_display("chat_view")
|
|
404
|
+
self.root.after(400, self.scroll_to_bottom)
|
|
405
|
+
|
|
406
|
+
def on_topic_select(self, e):
|
|
407
|
+
idx = self.topic_list.curselection()
|
|
408
|
+
if not idx:
|
|
409
|
+
return
|
|
410
|
+
tp = self.db.list_topics()[idx[0]]
|
|
411
|
+
self.switch_topic(tp["topic_id"])
|
|
412
|
+
|
|
413
|
+
def newline(self, e=None):
|
|
414
|
+
self.input_box.insert(tk.INSERT, "\n")
|
|
415
|
+
return "break"
|
|
416
|
+
|
|
417
|
+
def send(self, e=None):
|
|
418
|
+
if self.generating or not self.current_topic_id:
|
|
419
|
+
return "break"
|
|
420
|
+
msg = self.input_box.get("1.0", tk.END).strip()
|
|
421
|
+
if not msg:
|
|
422
|
+
return "break"
|
|
423
|
+
self.input_box.delete("1.0", tk.END)
|
|
424
|
+
|
|
425
|
+
self._switch_display("console")
|
|
426
|
+
|
|
427
|
+
self.log(f"你:{msg}", "user")
|
|
428
|
+
self.generating = True
|
|
429
|
+
self.log("AI:", "ai")
|
|
430
|
+
|
|
431
|
+
self._update_status("⏳ 生成中... (Ctrl+C 打断)")
|
|
432
|
+
|
|
433
|
+
def work():
|
|
434
|
+
try:
|
|
435
|
+
conv_id = self.db.save_msg(
|
|
436
|
+
self.current_topic_id, msg, "", False)
|
|
437
|
+
self.sess.set_conversation_id(conv_id)
|
|
438
|
+
|
|
439
|
+
ai_msg, is_func = self.sess.chat_stream(msg, self.safe_stream)
|
|
440
|
+
|
|
441
|
+
self.root.after(0, self._flush_stream_to_messages)
|
|
442
|
+
|
|
443
|
+
self.db.save_msg(self.current_topic_id, msg, ai_msg, is_func)
|
|
444
|
+
|
|
445
|
+
self.root.after(0, self._render_and_show_chat)
|
|
446
|
+
self.root.after(0, lambda: self._update_status("✅ 完成"))
|
|
447
|
+
except Exception as ex:
|
|
448
|
+
ai_msg = f"异常:{ex}"
|
|
449
|
+
self.safe_stream(ai_msg)
|
|
450
|
+
self.root.after(0, self._flush_stream_to_messages)
|
|
451
|
+
self.root.after(0, self._render_and_show_chat)
|
|
452
|
+
self.root.after(0, lambda: self._update_status(f"❌ 错误: {ex}"))
|
|
453
|
+
finally:
|
|
454
|
+
self.generating = False
|
|
455
|
+
self.safe_log("")
|
|
456
|
+
|
|
457
|
+
threading.Thread(target=work, daemon=True).start()
|
|
458
|
+
return "break"
|
|
459
|
+
|
|
460
|
+
def _render_and_show_chat(self):
|
|
461
|
+
"""渲染最终聊天信息并切换到 web 视图"""
|
|
462
|
+
self._render_chat()
|
|
463
|
+
self._switch_display("chat_view")
|
|
464
|
+
|
|
465
|
+
def interrupt(self, e=None):
|
|
466
|
+
if self.generating:
|
|
467
|
+
self.sess.interrupt()
|
|
468
|
+
self.safe_log("\n🛑 已打断", "tool")
|
|
469
|
+
self.generating = False
|
|
470
|
+
self.root.after(0, self._flush_stream_to_messages)
|
|
471
|
+
self.root.after(0, self._render_and_show_chat)
|
|
472
|
+
self._update_status("🛑 已打断")
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def main():
|
|
476
|
+
root = tk.Tk()
|
|
477
|
+
app = TkGUI(root)
|
|
478
|
+
root.mainloop()
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
if __name__ == "__main__":
|
|
482
|
+
main()
|