cnhkmcp 2.0.3__py3-none-any.whl → 2.1.0__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.
Files changed (39) hide show
  1. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242/README.md +38 -0
  2. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242/config.json +6 -0
  3. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242/get_knowledgeBase_tool/ace_lib.py +1510 -0
  4. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242/get_knowledgeBase_tool/fetch_all_datasets.py +157 -0
  5. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242/get_knowledgeBase_tool/fetch_all_documentation.py +132 -0
  6. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242/get_knowledgeBase_tool/fetch_all_operators.py +99 -0
  7. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242/get_knowledgeBase_tool/helpful_functions.py +180 -0
  8. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242/icon.ico +0 -0
  9. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242/icon.png +0 -0
  10. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242/knowledge/test.txt +1 -0
  11. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242/main.py +581 -0
  12. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242/process_knowledge_base.py +280 -0
  13. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242/rag_engine.py +265 -0
  14. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242/requirements.txt +12 -0
  15. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242/run.bat +3 -0
  16. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242/vector_db/chroma.sqlite3 +0 -0
  17. cnhkmcp/untracked/AI/321/206/320/261/320/234/321/211/320/255/320/262/321/206/320/237/320/242/321/204/342/225/227/342/225/242//321/211/320/266/320/246/321/206/320/274/320/261/321/210/342/224/220/320/240/321/210/320/261/320/234/321/206/320/231/320/243/321/205/342/225/235/320/220/321/206/320/230/320/241.py +265 -0
  18. cnhkmcp/untracked/APP/Tranformer/Transformer.py +2804 -11
  19. cnhkmcp/untracked/APP/Tranformer/output/Alpha_candidates.json +1524 -889
  20. cnhkmcp/untracked/APP/Tranformer/output/Alpha_generated_expressions_error.json +884 -111
  21. cnhkmcp/untracked/APP/Tranformer/output/Alpha_generated_expressions_success.json +442 -168
  22. cnhkmcp/untracked/APP/Tranformer/template_summary.txt +2775 -1
  23. cnhkmcp/untracked/APP/ace.log +2 -0
  24. cnhkmcp/untracked/APP/give_me_idea/fetch_all_datasets.py +157 -0
  25. cnhkmcp/untracked/APP/give_me_idea/fetch_all_operators.py +99 -0
  26. cnhkmcp/untracked/APP/simulator/simulator_wqb.py +16 -16
  27. cnhkmcp/untracked/APP/static/brain.js +61 -0
  28. cnhkmcp/untracked/APP/static/script.js +140 -0
  29. cnhkmcp/untracked/APP/templates/index.html +25 -4
  30. cnhkmcp/untracked/APP//321/210/342/224/220/320/240/321/210/320/261/320/234/321/206/320/231/320/243/321/205/342/225/235/320/220/321/206/320/230/320/241.py +70 -8
  31. {cnhkmcp-2.0.3.dist-info → cnhkmcp-2.1.0.dist-info}/METADATA +1 -1
  32. {cnhkmcp-2.0.3.dist-info → cnhkmcp-2.1.0.dist-info}/RECORD +36 -20
  33. cnhkmcp/untracked/APP/hkSimulator/ace.log +0 -0
  34. cnhkmcp/untracked/APP/hkSimulator/autosim_20251205_145240.log +0 -0
  35. cnhkmcp/untracked/APP/hkSimulator/autosim_20251215_030103.log +0 -0
  36. {cnhkmcp-2.0.3.dist-info → cnhkmcp-2.1.0.dist-info}/WHEEL +0 -0
  37. {cnhkmcp-2.0.3.dist-info → cnhkmcp-2.1.0.dist-info}/entry_points.txt +0 -0
  38. {cnhkmcp-2.0.3.dist-info → cnhkmcp-2.1.0.dist-info}/licenses/LICENSE +0 -0
  39. {cnhkmcp-2.0.3.dist-info → cnhkmcp-2.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,581 @@
1
+ import os
2
+ import json
3
+ import base64
4
+ import tkinter as tk
5
+ from tkinter import scrolledtext, messagebox, Toplevel
6
+ from PIL import Image, ImageTk, ImageGrab
7
+ import pyautogui
8
+ from openai import OpenAI
9
+ import threading
10
+ import io
11
+ import time
12
+ import ctypes
13
+ import subprocess
14
+ import sys
15
+
16
+ # --- Auto-Install Dependencies ---
17
+ def install_dependencies():
18
+ import importlib.util
19
+ import importlib.metadata
20
+
21
+ # Mapping of package names to their import names (if different)
22
+ packages = {
23
+ "openai": "openai",
24
+ "pyautogui": "pyautogui",
25
+ "Pillow": "PIL",
26
+ "pyperclip": "pyperclip",
27
+ "keyboard": "keyboard",
28
+ "fastembed": "fastembed",
29
+ "chromadb": "chromadb",
30
+ "watchdog": "watchdog",
31
+ "urllib3": "urllib3",
32
+ "pypdf": "pypdf",
33
+ "python-docx": "docx"
34
+ }
35
+
36
+ missing = []
37
+ for pkg_name, import_name in packages.items():
38
+ if importlib.util.find_spec(import_name) is None:
39
+ missing.append(pkg_name)
40
+
41
+ if missing:
42
+ print(f"Missing dependencies: {missing}. Installing...")
43
+ # Try Tsinghua source first
44
+ tsinghua_url = "https://pypi.tuna.tsinghua.edu.cn/simple"
45
+ try:
46
+ print(f"Attempting to install via Tsinghua mirror: {tsinghua_url}")
47
+ subprocess.check_call([sys.executable, "-m", "pip", "install", *missing, "-i", tsinghua_url])
48
+ print("Dependencies installed successfully via Tsinghua.")
49
+ except Exception as e:
50
+ print(f"Tsinghua mirror failed, falling back to default source: {e}")
51
+ try:
52
+ subprocess.check_call([sys.executable, "-m", "pip", "install", *missing])
53
+ print("Dependencies installed successfully via default source.")
54
+ except Exception as e2:
55
+ print(f"Failed to install dependencies: {e2}")
56
+ messagebox.showwarning("Warning", f"Failed to auto-install some dependencies: {e2}\nPlease run 'pip install -r requirements.txt' manually.")
57
+
58
+ # Run install check before other imports that might fail
59
+ install_dependencies()
60
+
61
+ # Now import our custom RAG engine
62
+ try:
63
+ from rag_engine import KnowledgeBase
64
+ except ImportError:
65
+ KnowledgeBase = None
66
+ print("KnowledgeBase module not found or dependencies missing.")
67
+
68
+ # Set DPI Awareness (Windows) to ensure high-resolution screenshots
69
+ try:
70
+ ctypes.windll.shcore.SetProcessDpiAwareness(1)
71
+ except Exception:
72
+ try:
73
+ ctypes.windll.user32.SetProcessDPIAware()
74
+ except Exception:
75
+ pass
76
+
77
+ # Load Configuration
78
+ CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'config.json')
79
+ ICON_PATH = os.path.join(os.path.dirname(__file__), 'icon.png')
80
+
81
+ def load_config():
82
+ if not os.path.exists(CONFIG_PATH):
83
+ messagebox.showerror("Error", "Config file not found!")
84
+ return None
85
+ with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
86
+ return json.load(f)
87
+
88
+ CONFIG = load_config()
89
+
90
+ class BrainConsultantApp:
91
+ def __init__(self, root):
92
+ self.root = root
93
+ self.root.title("BRAIN Consultant Assistant")
94
+
95
+ # Floating Icon Setup
96
+ self.root.overrideredirect(True) # Frameless
97
+ self.root.attributes("-topmost", True) # Always on top
98
+ self.root.geometry("64x64+100+100") # Small size, initial position
99
+
100
+ # Transparency setup (Windows only hack)
101
+ transparent_color = '#ff00ff'
102
+ self.root.configure(bg=transparent_color)
103
+ self.root.wm_attributes("-transparentcolor", transparent_color)
104
+
105
+ # Load Icon
106
+ self.icon_image = None
107
+ if os.path.exists(ICON_PATH):
108
+ try:
109
+ # Force RGBA to ensure we can handle transparency
110
+ img = Image.open(ICON_PATH).convert("RGBA")
111
+ img = img.resize((64, 64), Image.Resampling.LANCZOS)
112
+
113
+ # Fix for halo effect: Strict binary alpha
114
+ # Any pixel that is not fully opaque becomes fully transparent
115
+ # This removes the semi-transparent edges that blend with the background color
116
+ datas = img.getdata()
117
+ new_data = []
118
+ for item in datas:
119
+ if item[3] < 200: # Threshold: if alpha < 200, make it transparent
120
+ new_data.append((0, 0, 0, 0))
121
+ else:
122
+ # Keep original color, force full opacity
123
+ new_data.append((item[0], item[1], item[2], 255))
124
+ img.putdata(new_data)
125
+
126
+ self.icon_image = ImageTk.PhotoImage(img)
127
+ # Set window icon if possible (though frameless windows don't show it usually)
128
+ self.root.iconphoto(False, self.icon_image)
129
+ except Exception as e:
130
+ print(f"Failed to load icon: {e}")
131
+
132
+ # Create a label as the button
133
+ self.icon_label = tk.Label(root, image=self.icon_image, bg=transparent_color, cursor="hand2")
134
+ if not self.icon_image:
135
+ self.icon_label.config(text="BRAIN", fg="white", font=("Arial", 10, "bold"))
136
+ self.icon_label.pack(fill=tk.BOTH, expand=True)
137
+
138
+ # Bind events
139
+ self.icon_label.bind("<Button-3>", self.show_context_menu) # Right click menu
140
+ self.icon_label.bind("<ButtonPress-1>", self.start_move)
141
+ self.icon_label.bind("<ButtonRelease-1>", self.stop_move)
142
+ self.icon_label.bind("<B1-Motion>", self.do_move)
143
+
144
+ # Initialize OpenAI Client
145
+ self.client = OpenAI(
146
+ api_key=CONFIG['api_key'],
147
+ base_url=CONFIG['base_url']
148
+ )
149
+ self.model = CONFIG['model']
150
+ self.system_prompt = CONFIG.get('system_prompt', "You are a helpful assistant.")
151
+
152
+ # Initialize Knowledge Base
153
+ self.kb = None
154
+ if KnowledgeBase:
155
+ try:
156
+ self.kb = KnowledgeBase()
157
+ except Exception as e:
158
+ print(f"Failed to initialize Knowledge Base: {e}")
159
+
160
+ self.knowledge_dir = os.path.join(os.path.dirname(__file__), "knowledge")
161
+
162
+ # Last KB retrieval (for UI display)
163
+ self.last_kb_query = ""
164
+ self.last_kb_context = ""
165
+ self.last_kb_hits = []
166
+
167
+ self.current_screenshot = None
168
+ self.chat_window = None
169
+ self.history = [{"role": "system", "content": self.system_prompt}]
170
+
171
+ # Dragging state
172
+ self.x = 0
173
+ self.y = 0
174
+ self.dragging = False
175
+
176
+ def start_move(self, event):
177
+ self.x = event.x
178
+ self.y = event.y
179
+ self.dragging = False # Initialize as false, set to true if moved
180
+
181
+ def stop_move(self, event):
182
+ if not self.dragging:
183
+ self.start_snip()
184
+ self.dragging = False
185
+
186
+ def do_move(self, event):
187
+ self.dragging = True
188
+ deltax = event.x - self.x
189
+ deltay = event.y - self.y
190
+ x = self.root.winfo_x() + deltax
191
+ y = self.root.winfo_y() + deltay
192
+ self.root.geometry(f"+{x}+{y}")
193
+
194
+ def show_context_menu(self, event):
195
+ menu = tk.Menu(self.root, tearoff=0)
196
+ menu.add_command(label="💬 Chat Only", command=self.open_chat_window)
197
+ menu.add_separator()
198
+ menu.add_command(label="❌ Exit", command=self.root.quit)
199
+ menu.post(event.x_root, event.y_root)
200
+
201
+ def start_snip(self):
202
+ """Hides the window and takes a screenshot after a short delay."""
203
+ self.root.withdraw() # Hide main window
204
+ if self.chat_window and tk.Toplevel.winfo_exists(self.chat_window):
205
+ self.chat_window.withdraw()
206
+ self.root.after(500, self.take_screenshot)
207
+
208
+ def take_screenshot(self):
209
+ """Captures the full screen."""
210
+ try:
211
+ # Capture full screen
212
+ screenshot = ImageGrab.grab()
213
+ self.current_screenshot = screenshot
214
+
215
+ # Show the chat window with the screenshot
216
+ self.open_chat_window(with_screenshot=True)
217
+ except Exception as e:
218
+ messagebox.showerror("Error", f"Failed to take screenshot: {e}")
219
+ self.root.deiconify()
220
+
221
+ def open_chat_window(self, with_screenshot=False):
222
+ """Opens the chat interface."""
223
+ if self.chat_window is None or not tk.Toplevel.winfo_exists(self.chat_window):
224
+ self.chat_window = Toplevel(self.root)
225
+ self.chat_window.title("BRAIN Consultant Assistant - Chat")
226
+ self.chat_window.geometry("600x700")
227
+ self.chat_window.configure(bg="#1e1e1e") # Dark background
228
+ self.chat_window.attributes("-topmost", True) # Always on top
229
+ self.chat_window.protocol("WM_DELETE_WINDOW", self.on_chat_close)
230
+
231
+ if self.icon_image:
232
+ self.chat_window.iconphoto(False, self.icon_image)
233
+
234
+ # --- Layout Strategy: Pack Bottom-Up ---
235
+
236
+ # 1. Input Area (Bottom)
237
+ input_frame = tk.Frame(self.chat_window, bg="#1e1e1e")
238
+ input_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=10, pady=10)
239
+
240
+ # Button Frame
241
+ btn_frame = tk.Frame(input_frame, bg="#1e1e1e")
242
+ btn_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(5, 0))
243
+
244
+ # Buttons aligned to the right (visual order left→right): Open KB, KB hits, Resnip, Send
245
+ send_btn = tk.Button(
246
+ btn_frame,
247
+ text="提问",
248
+ command=self.send_message,
249
+ bg="#007acc",
250
+ fg="white",
251
+ font=("Segoe UI", 10, "bold"),
252
+ relief=tk.FLAT,
253
+ padx=15
254
+ )
255
+ send_btn.pack(side=tk.RIGHT, padx=5)
256
+
257
+ continue_snip_btn = tk.Button(
258
+ btn_frame,
259
+ text="📸 重新截屏",
260
+ command=self.start_snip,
261
+ bg="#3c3c3c",
262
+ fg="white",
263
+ font=("Segoe UI", 10),
264
+ relief=tk.FLAT,
265
+ padx=10
266
+ )
267
+ continue_snip_btn.pack(side=tk.RIGHT, padx=5)
268
+
269
+ self.kb_hits_btn = tk.Button(
270
+ btn_frame,
271
+ text="📚 命中内容",
272
+ command=self.show_kb_hits,
273
+ bg="#3c3c3c",
274
+ fg="white",
275
+ font=("Segoe UI", 10),
276
+ relief=tk.FLAT,
277
+ padx=10,
278
+ state=tk.DISABLED
279
+ )
280
+ self.kb_hits_btn.pack(side=tk.RIGHT, padx=5)
281
+
282
+ open_kb_btn = tk.Button(
283
+ btn_frame,
284
+ text="📂 打开知识库",
285
+ command=self.open_knowledge_folder,
286
+ bg="#3c3c3c",
287
+ fg="white",
288
+ font=("Segoe UI", 10),
289
+ relief=tk.FLAT,
290
+ padx=10
291
+ )
292
+ open_kb_btn.pack(side=tk.RIGHT, padx=5)
293
+
294
+ # Text Entry (Multi-line, Full Width)
295
+ self.msg_entry = tk.Text(
296
+ input_frame,
297
+ height=4, # Slightly taller
298
+ font=("Consolas", 11),
299
+ bg="#3c3c3c",
300
+ fg="white",
301
+ insertbackground="white",
302
+ relief=tk.FLAT,
303
+ padx=5,
304
+ pady=5
305
+ )
306
+ self.msg_entry.pack(side=tk.BOTTOM, fill=tk.X)
307
+ self.msg_entry.bind("<Return>", self.handle_return)
308
+ self.msg_entry.bind("<Shift-Return>", lambda e: None)
309
+
310
+ # 2. Screenshot Preview (Above Input)
311
+ self.image_label = tk.Label(self.chat_window, bg="#1e1e1e")
312
+ self.image_label.pack(side=tk.BOTTOM, pady=5)
313
+
314
+ # 3. Chat History (Top, fills remaining space)
315
+ chat_frame = tk.Frame(self.chat_window, bg="#252526")
316
+ chat_frame.pack(side=tk.TOP, expand=True, fill='both', padx=10, pady=10)
317
+
318
+ # Chat History Display (High-tech style)
319
+ self.chat_display = tk.Text(
320
+ chat_frame,
321
+ state='disabled',
322
+ wrap=tk.WORD,
323
+ bg="#252526",
324
+ fg="#d4d4d4",
325
+ font=("Consolas", 10),
326
+ insertbackground="white",
327
+ relief=tk.FLAT,
328
+ padx=10,
329
+ pady=10
330
+ )
331
+ self.chat_display.pack(side=tk.LEFT, expand=True, fill='both')
332
+
333
+ self.chat_display.tag_config("user", foreground="#569cd6", font=("Consolas", 10, "bold")) # Blue
334
+ self.chat_display.tag_config("assistant", foreground="#4ec9b0", font=("Consolas", 10)) # Teal
335
+ self.chat_display.tag_config("system", foreground="#6a9955", font=("Consolas", 9, "italic")) # Green
336
+
337
+ # Reset KB hit state for this window
338
+ self.last_kb_query = ""
339
+ self.last_kb_context = ""
340
+ if hasattr(self, "kb_hits_btn"):
341
+ self.kb_hits_btn.config(state=tk.DISABLED)
342
+
343
+
344
+
345
+ # If we just took a screenshot, display it
346
+ if with_screenshot and self.current_screenshot:
347
+ # Resize for preview
348
+ preview_img = self.current_screenshot.copy()
349
+ preview_img.thumbnail((500, 250))
350
+ self.photo = ImageTk.PhotoImage(preview_img)
351
+ self.image_label.config(image=self.photo)
352
+ self.image_label.image = self.photo
353
+ self.append_to_chat("System", "已截屏,顾问助手已准备好帮助您进行Alpha研究", "system")
354
+
355
+ # Auto-trigger analysis if user wants (optional, but "提问屏幕内容" implies user action)
356
+ # For now, we wait for user input or they can just click send with empty text to trigger analysis?
357
+ # Let's allow empty text to trigger "Analyze this"
358
+
359
+ elif not with_screenshot:
360
+ self.image_label.config(image='')
361
+ self.current_screenshot = None
362
+
363
+ self.chat_window.deiconify()
364
+ self.root.withdraw() # Keep main window hidden while chatting
365
+
366
+ def on_chat_close(self):
367
+ self.chat_window.destroy()
368
+ self.chat_window = None
369
+ self.current_screenshot = None
370
+ self.root.deiconify() # Show main window again
371
+
372
+ def append_to_chat(self, role, text, tag):
373
+ self.chat_display.config(state='normal')
374
+ self.chat_display.insert(tk.END, f"[{role}]: {text}\n\n", tag)
375
+ self.chat_display.see(tk.END)
376
+ self.chat_display.config(state='disabled')
377
+
378
+ def handle_return(self, event):
379
+ # If Shift is pressed, let default behavior happen (newline)
380
+ if event.state & 0x0001:
381
+ return None
382
+ # Otherwise send message
383
+ self.send_message()
384
+ return "break" # Prevent default newline
385
+
386
+ def send_message(self, event=None):
387
+ user_text = self.msg_entry.get("1.0", tk.END).strip()
388
+ user_typed_text = bool(user_text)
389
+
390
+ # Allow sending if there is a screenshot, even if text is empty (implies "Analyze this")
391
+ if not user_text and not self.current_screenshot:
392
+ return
393
+
394
+ if not user_text and self.current_screenshot:
395
+ user_text = "Please analyze this screenshot and guide me on the next steps."
396
+
397
+ # --- RAG: Query Knowledge Base ---
398
+ context = ""
399
+ hit_details = []
400
+ used_kb = False
401
+ if self.kb and user_typed_text:
402
+ try:
403
+ res = self.kb.query(user_text)
404
+ used_kb = bool(res.get("hit"))
405
+ context = res.get("context", "") if used_kb else ""
406
+ hit_details = res.get("hits", []) or []
407
+ except Exception as e:
408
+ print(f"KB query failed: {e}")
409
+ used_kb = False
410
+ context = ""
411
+ hit_details = []
412
+
413
+ # Save last KB retrieval and toggle button
414
+ self.last_kb_query = user_text
415
+ self.last_kb_context = context or ""
416
+ self.last_kb_hits = hit_details
417
+ if hasattr(self, "kb_hits_btn"):
418
+ self.kb_hits_btn.config(state=(tk.NORMAL if used_kb and context else tk.DISABLED))
419
+
420
+ # Show user message first
421
+ self.msg_entry.delete("1.0", tk.END)
422
+ self.append_to_chat("User", user_text, "user")
423
+
424
+ # Let user know whether KB was used (only when user actually typed text)
425
+ if user_typed_text:
426
+ if self.kb:
427
+ if used_kb:
428
+ self.append_to_chat("System", "已检索本地知识库:命中相关内容,将结合回答。", "system")
429
+ else:
430
+ self.append_to_chat("System", "已检索本地知识库:未命中,将直接基于模型回答。", "system")
431
+ else:
432
+ self.append_to_chat("System", "本地知识库未启用(依赖缺失或初始化失败),将直接基于模型回答。", "system")
433
+
434
+ # Augment user text with context if available
435
+ api_user_text = user_text
436
+ if context:
437
+ api_user_text = f"【参考本地知识库内容】:\n{context}\n\n【用户问题】:\n{user_text}"
438
+
439
+ # Prepare messages for API
440
+ messages = list(self.history) # Copy existing history
441
+
442
+ new_message = {"role": "user", "content": []}
443
+
444
+ # Add text (using the augmented text for the API, but showing original in UI)
445
+ if api_user_text:
446
+ new_message["content"].append({"type": "text", "text": api_user_text})
447
+
448
+ # Add image if it's the FIRST message about this screenshot
449
+ if self.current_screenshot:
450
+ base64_image = self.encode_image(self.current_screenshot)
451
+ new_message["content"].append({
452
+ "type": "image_url",
453
+ "image_url": {
454
+ "url": f"data:image/jpeg;base64,{base64_image}"
455
+ }
456
+ })
457
+ self.current_screenshot = None
458
+ self.image_label.config(image='') # Hide preview after sending
459
+
460
+ # Simplify content if just text
461
+ if len(new_message["content"]) == 1 and new_message["content"][0]["type"] == "text":
462
+ new_message["content"] = api_user_text
463
+
464
+ messages.append(new_message)
465
+
466
+ # Start thread for API call
467
+ threading.Thread(target=self.run_api_call, args=(messages,)).start()
468
+
469
+ def open_knowledge_folder(self):
470
+ target_dir = self.knowledge_dir or os.path.join(os.path.dirname(__file__), "knowledge")
471
+ if not os.path.exists(target_dir):
472
+ messagebox.showinfo("知识库", "知识库文件夹不存在。")
473
+ return
474
+ try:
475
+ if sys.platform.startswith("win"):
476
+ os.startfile(target_dir)
477
+ elif sys.platform == "darwin":
478
+ subprocess.Popen(["open", target_dir])
479
+ else:
480
+ subprocess.Popen(["xdg-open", target_dir])
481
+ except Exception as e:
482
+ messagebox.showerror("知识库", f"无法打开知识库文件夹:{e}")
483
+
484
+ def show_kb_hits(self):
485
+ """Show the last retrieved KB context in a separate window."""
486
+ if not self.kb:
487
+ messagebox.showinfo("知识库", "本地知识库未启用或初始化失败。")
488
+ return
489
+ if not getattr(self, "last_kb_context", ""):
490
+ messagebox.showinfo("知识库", "本次提问未命中知识库内容。")
491
+ return
492
+
493
+ win = Toplevel(self.chat_window if self.chat_window else self.root)
494
+ win.title("知识库命中内容")
495
+ win.geometry("700x500")
496
+ win.configure(bg="#1e1e1e")
497
+ win.attributes("-topmost", True)
498
+ if self.icon_image:
499
+ win.iconphoto(False, self.icon_image)
500
+
501
+ header = tk.Label(
502
+ win,
503
+ text=f"查询:{self.last_kb_query}",
504
+ bg="#1e1e1e",
505
+ fg="#d4d4d4",
506
+ font=("Segoe UI", 10, "bold"),
507
+ anchor="w",
508
+ justify="left",
509
+ padx=10,
510
+ pady=10
511
+ )
512
+ header.pack(side=tk.TOP, fill=tk.X)
513
+
514
+ text_box = scrolledtext.ScrolledText(
515
+ win,
516
+ wrap=tk.WORD,
517
+ bg="#252526",
518
+ fg="#d4d4d4",
519
+ insertbackground="white",
520
+ font=("Consolas", 10)
521
+ )
522
+ text_box.pack(side=tk.TOP, expand=True, fill="both", padx=10, pady=(0, 10))
523
+
524
+ # Prefer structured hits if available (shows source + score)
525
+ hits = getattr(self, "last_kb_hits", None) or []
526
+ if hits:
527
+ lines = []
528
+ for i, h in enumerate(hits, start=1):
529
+ src = h.get("source", "")
530
+ dist = h.get("distance", None)
531
+ dist_str = f"{dist:.4f}" if isinstance(dist, (int, float)) else "N/A"
532
+ lines.append(f"--- Hit {i} | source={src} | distance={dist_str} ---\n")
533
+ lines.append((h.get("text") or "") + "\n\n")
534
+ text_box.insert(tk.END, "".join(lines).strip())
535
+ else:
536
+ text_box.insert(tk.END, self.last_kb_context)
537
+ text_box.config(state='disabled')
538
+
539
+ def run_api_call(self, messages):
540
+ try:
541
+ # Create an empty message for the assistant first
542
+ self.root.after(0, self.append_to_chat, "顾问助手", "", "assistant")
543
+
544
+ stream = self.client.chat.completions.create(
545
+ model=self.model,
546
+ messages=messages,
547
+ temperature=0.6,
548
+ stream=True
549
+ )
550
+
551
+ full_response = ""
552
+ for chunk in stream:
553
+ if chunk.choices[0].delta.content:
554
+ content = chunk.choices[0].delta.content
555
+ full_response += content
556
+ # Update UI with the new chunk
557
+ self.root.after(0, self.update_last_message, content)
558
+
559
+ # Update history with full response
560
+ self.history.append(messages[-1]) # User msg
561
+ self.history.append({"role": "assistant", "content": full_response})
562
+
563
+ except Exception as e:
564
+ error_msg = str(e)
565
+ self.root.after(0, self.append_to_chat, "Error", error_msg, "system")
566
+
567
+ def update_last_message(self, text_chunk):
568
+ self.chat_display.config(state='normal')
569
+ self.chat_display.insert(tk.END, text_chunk, "assistant")
570
+ self.chat_display.see(tk.END)
571
+ self.chat_display.config(state='disabled')
572
+
573
+ def encode_image(self, image):
574
+ buffered = io.BytesIO()
575
+ image.save(buffered, format="JPEG")
576
+ return base64.b64encode(buffered.getvalue()).decode('utf-8')
577
+
578
+ if __name__ == "__main__":
579
+ root = tk.Tk()
580
+ app = BrainConsultantApp(root)
581
+ root.mainloop()