broadfield-dev commited on
Commit
4dff6be
·
verified ·
1 Parent(s): 0da85d3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +74 -30
app.py CHANGED
@@ -118,66 +118,108 @@ def process_user_interaction_gradio(user_input: str, provider_name: str, model_d
118
  initial_insights = retrieve_rules_semantic(f"{user_input}\n{history_str_for_prompt}", k=5)
119
  initial_insights_ctx_str, parsed_initial_insights_list = format_insights_for_prompt(initial_insights)
120
  logger.info(f"PUI_GRADIO [{request_id}]: Initial RAG (insights) found {len(initial_insights)}. Context: {initial_insights_ctx_str[:150]}...")
 
121
  action_type, action_input_dict = "quick_respond", {}
122
  user_input_lower = user_input.lower()
123
  time_before_tool_decision = time.time()
124
- if WEB_SEARCH_ENABLED and ("http://" in user_input or "https://" in user_input):
125
- url_match = re.search(r'(https?://[^\s]+)', user_input)
126
- if url_match: action_type, action_input_dict = "scrape_url_and_report", {"url": url_match.group(1)}
127
- if action_type == "quick_respond" and len(user_input.split()) <= 3 and any(kw in user_input_lower for kw in ["hello", "hi", "thanks", "ok", "bye"]) and not "?" in user_input: pass
128
- elif action_type == "quick_respond" and WEB_SEARCH_ENABLED and (len(user_input.split()) > 3 or "?" in user_input or any(w in user_input_lower for w in ["what is", "how to", "explain", "search for"])):
 
 
 
 
 
129
  yield "status", "<i>[LLM choosing best approach...]</i>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  history_snippet = "\n".join([f"{msg['role']}: {msg['content'][:100]}" for msg in chat_history_for_prompt[-2:]])
131
  guideline_snippet = initial_insights_ctx_str[:200].replace('\n', ' ')
132
-
133
- # --- MODIFIED: Improved prompts for the tool-decision LLM ---
134
- tool_sys_prompt = """You are a precise routing agent. Your job is to analyze the user's query and the conversation context, then select the single best action to provide an answer.
135
- Output ONLY a single, valid JSON object with "action" and "action_input" keys. Do not add any other text or explanations.
136
 
137
- Example: {"action": "search_duckduckgo_and_report", "action_input": {"search_engine_query": "latest AI research"}}
 
 
 
 
 
 
 
 
 
138
 
139
- Here are the available actions with descriptions of when to use them:
140
- - "quick_respond": Use for simple greetings, acknowledgements, or if the answer is obvious from the immediate context and requires no special tools.
141
- - "answer_using_conversation_memory": Use if the user's query refers to a previous conversation, asks you to "remember" or "recall" something, or seems like it could be answered by a past interaction you've had. This tool searches a database of your past conversations for relevant information.
142
- - "search_duckduckgo_and_report": Use for general knowledge questions, questions about current events, or when the user explicitly asks you to search the web for information.
143
- - "scrape_url_and_report": Use ONLY when the user provides a specific URL to read from.
144
  """
145
- tool_user_prompt = f"User Query: \"{user_input}\"\n\nRecent History:\n{history_snippet}\n\nGuidelines Snippet (for context):\n{guideline_snippet}...\n\nBased on the query and the action descriptions provided in the system prompt, select the single best action to take. Output the corresponding JSON object."
146
  tool_decision_messages = [{"role":"system", "content": tool_sys_prompt}, {"role":"user", "content": tool_user_prompt}]
147
- # --- END OF MODIFICATION ---
148
 
149
  tool_provider, tool_model_id = TOOL_DECISION_PROVIDER_ENV, TOOL_DECISION_MODEL_ID_ENV
150
  tool_model_display = next((dn for dn, mid in MODELS_BY_PROVIDER.get(tool_provider.lower(), {}).get("models", {}).items() if mid == tool_model_id), None)
151
  if not tool_model_display: tool_model_display = get_default_model_display_name_for_provider(tool_provider)
 
152
  if tool_model_display:
153
  try:
154
  logger.info(f"PUI_GRADIO [{request_id}]: Tool decision LLM: {tool_provider}/{tool_model_display}")
155
- tool_resp_chunks = list(call_model_stream(provider=tool_provider, model_display_name=tool_model_display, messages=tool_decision_messages, temperature=0.0, max_tokens=150))
156
  tool_resp_raw = "".join(tool_resp_chunks).strip()
157
  json_match_tool = re.search(r"\{.*\}", tool_resp_raw, re.DOTALL)
158
  if json_match_tool:
159
  action_data = json.loads(json_match_tool.group(0))
160
- action_type, action_input_dict = action_data.get("action", "quick_respond"), action_data.get("action_input", {})
 
161
  if not isinstance(action_input_dict, dict): action_input_dict = {}
162
  logger.info(f"PUI_GRADIO [{request_id}]: LLM Tool Decision: Action='{action_type}', Input='{action_input_dict}'")
163
- else: logger.warning(f"PUI_GRADIO [{request_id}]: Tool decision LLM non-JSON. Raw: {tool_resp_raw}")
164
- except Exception as e: logger.error(f"PUI_GRADIO [{request_id}]: Tool decision LLM error: {e}", exc_info=False)
165
- else: logger.error(f"No model for tool decision provider {tool_provider}.")
166
- elif action_type == "quick_respond" and not WEB_SEARCH_ENABLED and (len(user_input.split()) > 4 or "?" in user_input or any(w in user_input_lower for w in ["remember","recall"])):
167
- action_type="answer_using_conversation_memory"
 
 
 
 
 
 
 
168
  logger.info(f"PUI_GRADIO [{request_id}]: Tool decision logic took {time.time() - time_before_tool_decision:.3f}s. Action: {action_type}, Input: {action_input_dict}")
169
  yield "status", f"<i>[Path: {action_type}. Preparing response...]</i>"
170
  final_system_prompt_str, final_user_prompt_content_str = custom_system_prompt or DEFAULT_SYSTEM_PROMPT, ""
 
171
  if action_type == "quick_respond":
172
  final_system_prompt_str += " Respond directly using guidelines & history."
173
  final_user_prompt_content_str = f"History:\n{history_str_for_prompt}\nGuidelines:\n{initial_insights_ctx_str}\nQuery: \"{user_input}\"\nResponse:"
174
  elif action_type == "answer_using_conversation_memory":
175
  yield "status", "<i>[Searching conversation memory (semantic)...]</i>"
176
- retrieved_mems = retrieve_memories_semantic(f"User query: {user_input}\nContext:\n{history_str_for_prompt[-1000:]}", k=2)
177
- memory_context = "Relevant Past Interactions:\n" + "\n".join([f"- User:{m.get('user_input','')}->AI:{m.get('bot_response','')} (Takeaway:{m.get('metrics',{}).get('takeaway','N/A')})" for m in retrieved_mems]) if retrieved_mems else "No relevant past interactions found."
178
- final_system_prompt_str += " Respond using Memory Context, guidelines, & history."
179
- final_user_prompt_content_str = f"History:\n{history_str_for_prompt}\nGuidelines:\n{initial_insights_ctx_str}\nMemory Context:\n{memory_context}\nQuery: \"{user_input}\"\nResponse (use memory context if relevant):"
180
- elif WEB_SEARCH_ENABLED and action_type in ["search_duckduckgo_and_report", "scrape_url_and_report"]:
 
 
 
 
 
181
  query_or_url = action_input_dict.get("search_engine_query") if "search" in action_type else action_input_dict.get("url")
182
  if not query_or_url:
183
  final_system_prompt_str += " Respond directly (web action failed: no input)."
@@ -195,9 +237,10 @@ Here are the available actions with descriptions of when to use them:
195
  yield "status", "<i>[Synthesizing web report...]</i>"
196
  final_system_prompt_str += " Generate report/answer from web content, history, & guidelines. Cite URLs as [Source X]."
197
  final_user_prompt_content_str = f"History:\n{history_str_for_prompt}\nGuidelines:\n{initial_insights_ctx_str}\nWeb Content:\n{scraped_content}\nQuery: \"{user_input}\"\nReport/Response (cite sources [Source X]):"
198
- else:
199
  final_system_prompt_str += " Respond directly (unknown action path)."
200
  final_user_prompt_content_str = f"History:\n{history_str_for_prompt}\nGuidelines:\n{initial_insights_ctx_str}\nQuery: \"{user_input}\"\nResponse:"
 
201
  final_llm_messages = [{"role": "system", "content": final_system_prompt_str}, {"role": "user", "content": final_user_prompt_content_str}]
202
  logger.debug(f"PUI_GRADIO [{request_id}]: Final LLM System Prompt: {final_system_prompt_str[:200]}...")
203
  logger.debug(f"PUI_GRADIO [{request_id}]: Final LLM User Prompt Start: {final_user_prompt_content_str[:200]}...")
@@ -212,6 +255,7 @@ Here are the available actions with descriptions of when to use them:
212
  logger.info(f"PUI_GRADIO [{request_id}]: Finished. Total: {time.time() - process_start_time:.2f}s. Resp len: {len(final_bot_text)}")
213
  yield "final_response_and_insights", {"response": final_bot_text, "insights_used": parsed_initial_insights_list}
214
 
 
215
  def perform_post_interaction_learning(user_input: str, bot_response: str, provider: str, model_disp_name: str, insights_reflected: list[dict], api_key_override: str = None):
216
  task_id = os.urandom(4).hex()
217
  logger.info(f"POST_INTERACTION_LEARNING [{task_id}]: START User='{user_input[:40]}...', Bot='{bot_response[:40]}...'")
 
118
  initial_insights = retrieve_rules_semantic(f"{user_input}\n{history_str_for_prompt}", k=5)
119
  initial_insights_ctx_str, parsed_initial_insights_list = format_insights_for_prompt(initial_insights)
120
  logger.info(f"PUI_GRADIO [{request_id}]: Initial RAG (insights) found {len(initial_insights)}. Context: {initial_insights_ctx_str[:150]}...")
121
+
122
  action_type, action_input_dict = "quick_respond", {}
123
  user_input_lower = user_input.lower()
124
  time_before_tool_decision = time.time()
125
+
126
+ # --- REFACTORED TOOL-DECISION LOGIC ---
127
+
128
+ # Heuristic for simple interactions that don't need a tool-decision LLM call
129
+ is_simple_interaction = len(user_input.split()) <= 3 and any(kw in user_input_lower for kw in ["hello", "hi", "thanks", "ok", "bye"]) and not "?" in user_input
130
+
131
+ if is_simple_interaction:
132
+ action_type = "quick_respond"
133
+ else:
134
+ # For any non-trivial interaction, use an LLM to decide the best tool.
135
  yield "status", "<i>[LLM choosing best approach...]</i>"
136
+
137
+ # 1. Define all possible tools and their descriptions
138
+ tool_definitions = {
139
+ "answer_using_conversation_memory": "Use if the user's query refers to a previous conversation, asks you to 'remember' or 'recall' something specific, or seems like it could be answered by a past interaction you've had. This tool searches a database of your past conversations.",
140
+ "search_duckduckgo_and_report": "Use for general knowledge questions, questions about current events, or when the user explicitly asks you to search the web for information.",
141
+ "scrape_url_and_report": "Use ONLY when the user provides a specific URL to read from.",
142
+ "quick_respond": "Use as a fallback for simple greetings, acknowledgements, or if the answer is obvious from the immediate context and requires no special tools."
143
+ }
144
+
145
+ # 2. Build the list of available tools for this specific run
146
+ available_tool_names = ["quick_respond", "answer_using_conversation_memory"]
147
+ if WEB_SEARCH_ENABLED:
148
+ available_tool_names.insert(1, "search_duckduckgo_and_report") # Give search higher priority
149
+ available_tool_names.insert(2, "scrape_url_and_report")
150
+
151
+ # 3. Create the prompt with the dynamic list of tools and their descriptions
152
+ tool_descriptions_for_prompt = "\n".join(f'- "{name}": {tool_definitions[name]}' for name in available_tool_names)
153
+
154
+ tool_sys_prompt = "You are a precise routing agent. Your job is to analyze the user's query and the conversation context, then select the single best action to provide an answer. Output ONLY a single, valid JSON object with 'action' and 'action_input' keys. Do not add any other text or explanations."
155
  history_snippet = "\n".join([f"{msg['role']}: {msg['content'][:100]}" for msg in chat_history_for_prompt[-2:]])
156
  guideline_snippet = initial_insights_ctx_str[:200].replace('\n', ' ')
 
 
 
 
157
 
158
+ tool_user_prompt = f"""User Query: "{user_input}"
159
+
160
+ Recent History:
161
+ {history_snippet}
162
+
163
+ Guidelines Snippet (for context):
164
+ {guideline_snippet}
165
+
166
+ Available Actions and their descriptions:
167
+ {tool_descriptions_for_prompt}
168
 
169
+ Based on the query and the action descriptions, select the single best action to take. Output the corresponding JSON object.
170
+ Example for web search: {{"action": "search_duckduckgo_and_report", "action_input": {{"search_engine_query": "latest AI research"}}}}
171
+ Example for memory recall: {{"action": "answer_using_conversation_memory", "action_input": {{}}}}
 
 
172
  """
173
+
174
  tool_decision_messages = [{"role":"system", "content": tool_sys_prompt}, {"role":"user", "content": tool_user_prompt}]
 
175
 
176
  tool_provider, tool_model_id = TOOL_DECISION_PROVIDER_ENV, TOOL_DECISION_MODEL_ID_ENV
177
  tool_model_display = next((dn for dn, mid in MODELS_BY_PROVIDER.get(tool_provider.lower(), {}).get("models", {}).items() if mid == tool_model_id), None)
178
  if not tool_model_display: tool_model_display = get_default_model_display_name_for_provider(tool_provider)
179
+
180
  if tool_model_display:
181
  try:
182
  logger.info(f"PUI_GRADIO [{request_id}]: Tool decision LLM: {tool_provider}/{tool_model_display}")
183
+ tool_resp_chunks = list(call_model_stream(provider=tool_provider, model_display_name=tool_model_display, messages=tool_decision_messages, temperature=0.0, max_tokens=200))
184
  tool_resp_raw = "".join(tool_resp_chunks).strip()
185
  json_match_tool = re.search(r"\{.*\}", tool_resp_raw, re.DOTALL)
186
  if json_match_tool:
187
  action_data = json.loads(json_match_tool.group(0))
188
+ action_type = action_data.get("action", "quick_respond")
189
+ action_input_dict = action_data.get("action_input", {})
190
  if not isinstance(action_input_dict, dict): action_input_dict = {}
191
  logger.info(f"PUI_GRADIO [{request_id}]: LLM Tool Decision: Action='{action_type}', Input='{action_input_dict}'")
192
+ else:
193
+ logger.warning(f"PUI_GRADIO [{request_id}]: Tool decision LLM non-JSON. Defaulting to quick_respond. Raw: {tool_resp_raw}")
194
+ action_type = "quick_respond" # Fallback
195
+ except Exception as e:
196
+ logger.error(f"PUI_GRADIO [{request_id}]: Tool decision LLM error. Defaulting to quick_respond: {e}", exc_info=False)
197
+ action_type = "quick_respond" # Fallback
198
+ else:
199
+ logger.error(f"No model for tool decision provider {tool_provider}. Defaulting to quick_respond.")
200
+ action_type = "quick_respond" # Fallback
201
+
202
+ # --- END OF REFACTORED LOGIC ---
203
+
204
  logger.info(f"PUI_GRADIO [{request_id}]: Tool decision logic took {time.time() - time_before_tool_decision:.3f}s. Action: {action_type}, Input: {action_input_dict}")
205
  yield "status", f"<i>[Path: {action_type}. Preparing response...]</i>"
206
  final_system_prompt_str, final_user_prompt_content_str = custom_system_prompt or DEFAULT_SYSTEM_PROMPT, ""
207
+
208
  if action_type == "quick_respond":
209
  final_system_prompt_str += " Respond directly using guidelines & history."
210
  final_user_prompt_content_str = f"History:\n{history_str_for_prompt}\nGuidelines:\n{initial_insights_ctx_str}\nQuery: \"{user_input}\"\nResponse:"
211
  elif action_type == "answer_using_conversation_memory":
212
  yield "status", "<i>[Searching conversation memory (semantic)...]</i>"
213
+ retrieved_mems = retrieve_memories_semantic(f"User query: {user_input}\nContext:\n{history_str_for_prompt[-1000:]}", k=3)
214
+ if retrieved_mems:
215
+ logger.info(f"PUI_GRADIO [{request_id}]: Found {len(retrieved_mems)} relevant memories.")
216
+ memory_context = "Relevant Past Interactions (for your context only, do not repeat verbatim):\n" + "\n".join([f"- User asked: '{m.get('user_input','')}'. You responded: '{m.get('bot_response','')}'. (Key takeaway: {m.get('metrics',{}).get('takeaway','N/A')})" for m in retrieved_mems])
217
+ else:
218
+ logger.info(f"PUI_GRADIO [{request_id}]: No relevant memories found for the query.")
219
+ memory_context = "No relevant past interactions were found in the memory database."
220
+ final_system_prompt_str += " You MUST use the provided 'Memory Context' to inform your answer. Synthesize the information from the memory with the current conversation history to respond to the user's query."
221
+ final_user_prompt_content_str = f"History:\n{history_str_for_prompt}\n\nGuidelines:\n{initial_insights_ctx_str}\n\nMemory Context:\n{memory_context}\n\nUser's Query: \"{user_input}\"\n\nResponse (use the Memory Context to answer the query):"
222
+ elif action_type in ["search_duckduckgo_and_report", "scrape_url_and_report"]:
223
  query_or_url = action_input_dict.get("search_engine_query") if "search" in action_type else action_input_dict.get("url")
224
  if not query_or_url:
225
  final_system_prompt_str += " Respond directly (web action failed: no input)."
 
237
  yield "status", "<i>[Synthesizing web report...]</i>"
238
  final_system_prompt_str += " Generate report/answer from web content, history, & guidelines. Cite URLs as [Source X]."
239
  final_user_prompt_content_str = f"History:\n{history_str_for_prompt}\nGuidelines:\n{initial_insights_ctx_str}\nWeb Content:\n{scraped_content}\nQuery: \"{user_input}\"\nReport/Response (cite sources [Source X]):"
240
+ else: # Fallback for unknown action
241
  final_system_prompt_str += " Respond directly (unknown action path)."
242
  final_user_prompt_content_str = f"History:\n{history_str_for_prompt}\nGuidelines:\n{initial_insights_ctx_str}\nQuery: \"{user_input}\"\nResponse:"
243
+
244
  final_llm_messages = [{"role": "system", "content": final_system_prompt_str}, {"role": "user", "content": final_user_prompt_content_str}]
245
  logger.debug(f"PUI_GRADIO [{request_id}]: Final LLM System Prompt: {final_system_prompt_str[:200]}...")
246
  logger.debug(f"PUI_GRADIO [{request_id}]: Final LLM User Prompt Start: {final_user_prompt_content_str[:200]}...")
 
255
  logger.info(f"PUI_GRADIO [{request_id}]: Finished. Total: {time.time() - process_start_time:.2f}s. Resp len: {len(final_bot_text)}")
256
  yield "final_response_and_insights", {"response": final_bot_text, "insights_used": parsed_initial_insights_list}
257
 
258
+ # The rest of the app.py file remains the same...
259
  def perform_post_interaction_learning(user_input: str, bot_response: str, provider: str, model_disp_name: str, insights_reflected: list[dict], api_key_override: str = None):
260
  task_id = os.urandom(4).hex()
261
  logger.info(f"POST_INTERACTION_LEARNING [{task_id}]: START User='{user_input[:40]}...', Bot='{bot_response[:40]}...'")