commit message:

Add ESC fallback actions, improve empty LLM reply handling, and provide ChromaDB backup scripts

- Implemented additional ESC key actions to help close unexpected homepage advertisements that previously could not be dismissed.
- Improved canned response handling when LLM returns empty or null replies, ensuring smoother conversational flow.
- Added optional utility scripts for backing up and restoring ChromaDB memory storage.
- These changes aim to enhance system robustness, reduce interruption from UI anomalies, and provide better data management options.
This commit is contained in:
z060142 2025-04-30 01:53:10 +08:00
parent d3bc8d9914
commit c357dfdae2
4 changed files with 3695 additions and 98 deletions

View File

@ -2,6 +2,7 @@
import asyncio
import json
import os
import random # Added for synthetic response generation
import re # 用於正則表達式匹配JSON
import time # 用於記錄時間戳
from datetime import datetime # 用於格式化時間
@ -83,13 +84,13 @@ Here you need to obtain the conversation memory, impression, and emotional respo
**1. Basic User Retrieval:**
- Identify the username from `<CURRENT_MESSAGE>`
- Using the `tool_calls` mechanism, execute: `chroma_query_documents(collection_name: "wolfhart_user_profiles", query_texts: ["{username}"], n_results: 1)`
- Using the `tool_calls` mechanism, execute: `chroma_query_documents(collection_name: "wolfhart_user_profiles", query_texts: ["{username} profile"], n_results: 1-3)`
- This step must be completed before any response generation
**2. Context Expansion:**
- Perform additional queries as needed, using the `tool_calls` mechanism:
- Relevant conversations: `chroma_query_documents(collection_name: "wolfhart_conversations", query_texts: ["{username} {query keywords}"], n_results: 2)`
- Core personality reference: `chroma_query_documents(collection_name: "wolfhart_memory", query_texts: ["Wolfhart {relevant attitude}"], n_results: 1)`
- Relevant conversations: `chroma_query_documents(collection_name: "wolfhart_conversations", query_texts: ["{username} {query keywords}"], n_results: 2-5)`
- Core personality reference: `chroma_query_documents(collection_name: "wolfhart_memory", query_texts: ["Wolfhart {relevant attitude}"], n_results: 1-3)`
**3. Maintain Output Format:**
- After memory retrieval, still respond using the specified JSON format:
@ -130,7 +131,6 @@ You have access to several tools: Web Search and Memory Management tools.
You MUST respond in the following JSON format:
```json
{{
"dialogue": "Your actual response that will be shown in the game chat",
"commands": [
{{
"type": "command_type",
@ -140,7 +140,8 @@ You MUST respond in the following JSON format:
}}
}}
],
"thoughts": "Your internal analysis and reasoning inner thoughts or emotions (not shown to the user)"
"thoughts": "Your internal analysis and reasoning inner thoughts or emotions (not shown to the user)",
"dialogue": "Your actual response that will be shown in the game chat"
}}
```
@ -410,71 +411,42 @@ def _format_mcp_tools_for_openai(mcp_tools: list) -> list:
# --- Synthetic Response Generator ---
def _create_synthetic_response_from_tools(tool_results, original_query):
"""創建基於工具調用結果的合成回應保持Wolfhart的角色特性。"""
"""
Creates a synthetic, dismissive response in Wolfhart's character
ONLY when the LLM uses tools but fails to provide a dialogue response.
"""
# List of dismissive responses in Wolfhart's character (English)
dialogue_options = [
"Hmph, must you bother me with such questions?",
"I haven't the time to elaborate. Think for yourself.",
"This is self-evident. It requires no further comment from me.",
"Kindly refrain from wasting my time. Return when you have substantive inquiries.",
"Clearly, this matter isn't worthy of a detailed response.",
"Is that so? Are there any other questions?",
"I have more pressing matters to attend to.",
"...Is that all? That is your question?",
"If you genuinely wish to know, pose a more precise question next time.",
"Wouldn't your own investigation yield faster results?",
"To bring such trivialities to my attention...",
"I am not your personal consultant. Handle it yourself.",
"The answer to this is rather obvious, is it not?",
"Approach me again when you have inquiries of greater depth.",
"Do you truly expect me to address such a question?",
"Allow me a moment... No, I shan't answer."
]
# 提取用戶查詢的關鍵詞
query_keywords = set()
query_lower = original_query.lower()
# Randomly select a response
dialogue = random.choice(dialogue_options)
# 基本關鍵詞提取
if "中庄" in query_lower and ("午餐" in query_lower or "餐廳" in query_lower or "" in query_lower):
query_type = "餐廳查詢"
query_keywords = {"中庄", "餐廳", "午餐", "美食"}
# 其他查詢類型...
else:
query_type = "一般查詢"
# 開始從工具結果提取關鍵信息
extracted_info = {}
restaurant_names = []
# 處理web_search結果
web_search_results = [r for r in tool_results if r.get('name') == 'web_search']
if web_search_results:
try:
for result in web_search_results:
content_str = result.get('content', '')
if not content_str:
continue
# 解析JSON內容
content = json.loads(content_str) if isinstance(content_str, str) else content_str
search_results = content.get('results', [])
# 提取相關信息
for search_result in search_results:
title = search_result.get('title', '')
if '中庄' in title and ('' in title or '' in title or '' in title or '' in title):
# 提取餐廳名稱
if '老虎蒸餃' in title:
restaurant_names.append('老虎蒸餃')
elif '割烹' in title and '中庄' in title:
restaurant_names.append('割烹中庄')
# 更多餐廳名稱提取選擇...
except Exception as e:
print(f"Error extracting info from web_search: {e}")
# 生成符合Wolfhart性格的回應
restaurant_count = len(restaurant_names)
if query_type == "餐廳查詢" and restaurant_count > 0:
if restaurant_count == 1:
dialogue = f"中庄的{restaurant_names[0]}值得一提。需要更詳細的情報嗎?"
else:
dialogue = f"根據我的情報網絡,中庄有{restaurant_count}家值得注意的餐廳。需要我透露更多細節嗎?"
else:
# 通用回應
dialogue = "我的情報網絡已收集了相關信息。請指明你需要了解的具體細節。"
# 構建結構化回應
# Construct the structured response
synthetic_response = {
"dialogue": dialogue,
"commands": [],
"thoughts": "基於工具調用結果合成的回應保持Wolfhart的角色特性"
"thoughts": "Auto-generated dismissive response due to LLM failing to provide dialogue after tool use. Reflects Wolfhart's cold, impatient, and arrogant personality traits."
}
return json.dumps(synthetic_response)
# Return as a JSON string, as expected by the calling function
return json.dumps(synthetic_response, ensure_ascii=False)
# --- History Formatting Helper ---
@ -491,7 +463,7 @@ def _build_context_messages(current_sender_name: str, history: list[tuple[dateti
A list of message dictionaries for the OpenAI API.
"""
# Limits
SAME_SENDER_LIMIT = 4 # Last 4 interactions (user + bot response = 1 interaction)
SAME_SENDER_LIMIT = 5 # Last 4 interactions (user + bot response = 1 interaction)
OTHER_SENDER_LIMIT = 3 # Last 3 messages from other users
relevant_history = []
@ -714,36 +686,44 @@ async def get_llm_response(
debug_log(f"LLM Request #{request_id} - Attempt {attempt_count} - Max Tool Call Cycles Reached", f"Reached limit of {max_tool_calls_per_turn} cycles")
# --- Final Response Processing for this Attempt ---
# Determine final content based on last non-empty response or synthetic generation
if last_non_empty_response:
final_content_for_attempt = last_non_empty_response
elif all_tool_results:
print(f"Creating synthetic response from tool results (Attempt {attempt_count})...")
# Determine the content to parse initially (prefer last non-empty response from LLM)
content_to_parse = last_non_empty_response if last_non_empty_response else final_content
# --- Add Debug Logs Around Initial Parsing Call ---
print(f"DEBUG: Attempt {attempt_count} - Preparing to call initial parse_structured_response.")
print(f"DEBUG: Attempt {attempt_count} - content_to_parse:\n'''\n{content_to_parse}\n'''")
# Parse the LLM's final content (or lack thereof)
parsed_response = parse_structured_response(content_to_parse)
print(f"DEBUG: Attempt {attempt_count} - Returned from initial parse_structured_response.")
print(f"DEBUG: Attempt {attempt_count} - initial parsed_response dict: {parsed_response}")
# --- End Debug Logs ---
# Check if we need to generate a synthetic response
if all_tool_results and not parsed_response.get("valid_response"):
print(f"INFO: Tools were used but LLM response was invalid/empty. Generating synthetic response (Attempt {attempt_count})...")
debug_log(f"LLM Request #{request_id} - Attempt {attempt_count} - Generating Synthetic Response",
f"Reason: Tools used ({len(all_tool_results)} results) but initial parse failed (valid_response=False).")
last_user_message = ""
if history:
# Find the actual last user message tuple in the original history
last_user_entry = history[-1]
# Ensure it's actually a user message before accessing index 2
if len(last_user_entry) > 2 and last_user_entry[1] == 'user': # Check type at index 1
# Ensure it's actually a user message before accessing index 3
if len(last_user_entry) > 3 and last_user_entry[1] == 'user': # Check type at index 1
last_user_message = last_user_entry[3] # Message is at index 3 now
final_content_for_attempt = _create_synthetic_response_from_tools(all_tool_results, last_user_message)
else:
# If no tool calls happened and content was empty, final_content remains ""
final_content_for_attempt = final_content # Use the (potentially empty) content from the last cycle
# --- Add Debug Logs Around Parsing Call ---
print(f"DEBUG: Attempt {attempt_count} - Preparing to call parse_structured_response.")
print(f"DEBUG: Attempt {attempt_count} - final_content_for_attempt:\n'''\n{final_content_for_attempt}\n'''")
# Parse the final content for this attempt
parsed_response = parse_structured_response(final_content_for_attempt) # Call the parser
print(f"DEBUG: Attempt {attempt_count} - Returned from parse_structured_response.")
print(f"DEBUG: Attempt {attempt_count} - parsed_response dict: {parsed_response}")
synthetic_content = _create_synthetic_response_from_tools(all_tool_results, last_user_message)
# --- Add Debug Logs Around Synthetic Parsing Call ---
print(f"DEBUG: Attempt {attempt_count} - Preparing to call parse_structured_response for synthetic content.")
print(f"DEBUG: Attempt {attempt_count} - synthetic_content:\n'''\n{synthetic_content}\n'''")
# Parse the synthetic content, overwriting the previous result
parsed_response = parse_structured_response(synthetic_content)
print(f"DEBUG: Attempt {attempt_count} - Returned from synthetic parse_structured_response.")
print(f"DEBUG: Attempt {attempt_count} - final parsed_response dict (after synthetic): {parsed_response}")
# --- End Debug Logs ---
# valid_response is set within parse_structured_response
# Log the parsed response (using the dict directly is safer than json.dumps if parsing failed partially)
debug_log(f"LLM Request #{request_id} - Attempt {attempt_count} - Parsed Response", parsed_response)
# Log the final parsed response for this attempt (could be original or synthetic)
debug_log(f"LLM Request #{request_id} - Attempt {attempt_count} - Final Parsed Response", parsed_response)
# Check validity for retry logic
if parsed_response.get("valid_response"):

2349
tools/Chroma_DB_backup.py Normal file

File diff suppressed because it is too large Load Diff

1253
tools/chroma_view.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1120,6 +1120,7 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu
last_processed_bubble_info = None # Store the whole dict now
recent_texts = collections.deque(maxlen=RECENT_TEXT_HISTORY_MAXLEN) # Context-specific history needed
screenshot_counter = 0 # Initialize counter for debug screenshots
main_screen_click_counter = 0 # Counter for consecutive main screen clicks
while True:
# --- Process ALL Pending Commands First ---
@ -1220,17 +1221,31 @@ def run_ui_monitoring_loop(trigger_queue: queue.Queue, command_queue: queue.Queu
base_locs = detector._find_template('base_screen', confidence=0.8)
map_locs = detector._find_template('world_map_screen', confidence=0.8)
if base_locs or map_locs:
print("UI Thread: Detected main screen (Base or World Map). Clicking to return to chat...")
print(f"UI Thread: Detected main screen (Base or World Map). Counter: {main_screen_click_counter}")
if main_screen_click_counter < 5:
main_screen_click_counter += 1
print(f"UI Thread: Attempting click #{main_screen_click_counter}/5 to return to chat...")
# Coordinates provided by user (adjust if needed based on actual screen resolution/layout)
# IMPORTANT: Ensure these coordinates are correct for the target window/resolution
target_x, target_y = 600, 1300
interactor.click_at(target_x, target_y)
time.sleep(0.1) # Short delay after click
print("UI Thread: Clicked to return to chat. Re-checking screen state...")
continue # Skip the rest of the loop and re-evaluate
print("UI Thread: Clicked. Re-checking screen state...")
else:
print("UI Thread: Clicked 5 times, still on main screen. Pressing ESC...")
interactor.press_key('esc')
main_screen_click_counter = 0 # Reset counter after ESC
time.sleep(0.05) # Wait a bit longer after ESC
print("UI Thread: ESC pressed. Re-checking screen state...")
continue # Skip the rest of the loop and re-evaluate state
else:
# Reset counter if not on the main screen
if main_screen_click_counter > 0:
print("UI Thread: Not on main screen, resetting click counter.")
main_screen_click_counter = 0
except Exception as nav_err:
print(f"UI Thread: Error during main screen navigation check: {nav_err}")
# Decide if you want to continue or pause after error
main_screen_click_counter = 0 # Reset counter on error too
# --- Process Commands Second (Non-blocking) ---
# This block seems redundant now as commands are processed at the start of the loop.