tea-agent 0.2.4__py3-none-any.whl

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/__init__.py ADDED
@@ -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()