jesusvilela commited on
Commit
05e4e0d
·
verified ·
1 Parent(s): eea9aff

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +54 -48
app.py CHANGED
@@ -78,36 +78,32 @@ google_genai_client: Optional[google_genai_sdk.Client] = None
78
  try:
79
  from langgraph.graph import StateGraph, END
80
  try:
81
- from langgraph.prebuilt import ToolNode # Try modern ToolNode first
82
  LG_ToolExecutor_Class = ToolNode
83
  print("Using langgraph.prebuilt.ToolNode for LangGraph tool execution.")
84
  except ImportError:
85
  try:
86
- from langgraph.prebuilt import ToolExecutor # Fallback to older ToolExecutor
87
  LG_ToolExecutor_Class = ToolExecutor
88
  print("Using langgraph.prebuilt.ToolExecutor (fallback) for LangGraph tool execution.")
89
  except ImportError as e_lg_exec_inner:
90
  print(f"Failed to import ToolNode and ToolExecutor from langgraph.prebuilt: {e_lg_exec_inner}")
91
  LG_ToolExecutor_Class = None
92
 
93
- if LG_ToolExecutor_Class is not None: # Only proceed if a tool executor class was found
94
  try:
95
  from langgraph.prebuilt import ToolInvocation as LGToolInvocationActual
96
  except ImportError:
97
  try:
98
- from langgraph.tools import ToolInvocation as LGToolInvocationActual # Older path
99
- print("Imported ToolInvocation from langgraph.tools (older path).")
100
  except ImportError as e_ti:
101
- print(f"WARNING: Could not import ToolInvocation from langgraph.prebuilt or langgraph.tools: {e_ti}. LangGraph may not function fully if ToolExecutor_Class requires it.")
102
- LGToolInvocationActual = None # type: ignore
103
 
104
- # Other essential LangGraph imports
105
- from langgraph.graph.message import add_messages as lg_add_messages
106
- from langgraph.checkpoint.memory import MemorySaver as LGMemorySaver
107
-
108
- # Set LANGGRAPH_FLAVOR_AVAILABLE only if all critical components for your setup are loaded.
109
- # If LGToolInvocationActual is critical for your chosen LG_ToolExecutor_Class, check it here.
110
- if LGToolInvocationActual is not None or type(LG_ToolExecutor_Class).__name__ == 'ToolNode': # ToolNode might not need separate ToolInvocation
111
  LANGGRAPH_FLAVOR_AVAILABLE = True
112
  LG_StateGraph, LG_END, LG_ToolInvocation, add_messages, MemorySaver_Class = \
113
  StateGraph, END, LGToolInvocationActual, lg_add_messages, LGMemorySaver # type: ignore
@@ -115,14 +111,14 @@ try:
115
  else:
116
  LANGGRAPH_FLAVOR_AVAILABLE = False
117
  LG_StateGraph, LG_END, add_messages, MemorySaver_Class = (None,) * 4 # type: ignore
118
- print(f"WARNING: LangGraph ToolInvocation not found and may be required. LangGraph agent functionality might be limited or disabled.")
119
 
120
- else: # No LG_ToolExecutor_Class found
121
  LANGGRAPH_FLAVOR_AVAILABLE = False
122
  LG_StateGraph, LG_END, LG_ToolInvocation, add_messages, MemorySaver_Class = (None,) * 5 # type: ignore
123
  print(f"WARNING: No suitable LangGraph tool executor (ToolNode/ToolExecutor) found. LangGraph agent will be disabled.")
124
 
125
- except ImportError as e: # Catch import error for StateGraph, END itself
126
  LANGGRAPH_FLAVOR_AVAILABLE = False
127
  LG_StateGraph, LG_ToolExecutor_Class, LG_END, LG_ToolInvocation, add_messages, MemorySaver_Class = (None,) * 6
128
  print(f"WARNING: Core LangGraph components (like StateGraph, END) not found or import error: {e}. LangGraph agent will be disabled.")
@@ -259,7 +255,7 @@ def _download_file(file_identifier: str, task_id_for_file: Optional[str] = None)
259
  name_without_ext, current_ext = os.path.splitext(effective_save_path)
260
  if not current_ext:
261
  content_type_header = r.headers.get('content-type', '')
262
- content_type_val = content_type_header.split(';').strip() if content_type_header else ''
263
  if content_type_val:
264
  guessed_ext = mimetypes.guess_extension(content_type_val)
265
  if guessed_ext: effective_save_path += guessed_ext; logger.info(f"Added guessed ext: {guessed_ext}")
@@ -400,9 +396,9 @@ def initialize_agent_and_tools(force_reinit=False):
400
  model=GEMINI_MODEL_NAME,
401
  google_api_key=GOOGLE_API_KEY,
402
  temperature=0.0,
403
- # safety_settings is removed to use model defaults.
404
  timeout=120,
405
- convert_system_message_to_human=False # Explicitly False
406
  )
407
  logger.info(f"LangChain LLM (Planner) initialized: {GEMINI_MODEL_NAME} (Using default safety settings, convert_system_message_to_human=False)")
408
  except Exception as e:
@@ -421,61 +417,68 @@ def initialize_agent_and_tools(force_reinit=False):
421
  except Exception as e: logger.warning(f"PythonREPLTool init failed: {e}")
422
  logger.info(f"Final tools list for agent: {[t.name for t in TOOLS]}")
423
 
424
- if LANGGRAPH_FLAVOR_AVAILABLE and all([LG_StateGraph, LG_ToolExecutor_Class, LG_END, LLM_INSTANCE, add_messages]): # LG_ToolInvocation removed from check as it's problematic
425
  if not LANGGRAPH_MEMORY_SAVER and MemorySaver_Class: LANGGRAPH_MEMORY_SAVER = MemorySaver_Class(); logger.info("LangGraph MemorySaver initialized.")
426
  try:
427
  logger.info(f"Attempting LangGraph init (Tool Executor type: {LG_ToolExecutor_Class.__name__ if LG_ToolExecutor_Class else 'None'})")
428
  _TypedDict = getattr(__import__('typing_extensions'), 'TypedDict', dict)
429
  class AgentState(_TypedDict): input: str; messages: Annotated[List[Any], add_messages]
430
 
431
- base_system_prompt_content = LANGGRAPH_PROMPT_TEMPLATE_STR.format(
432
  tools="\n".join([f"- {t.name}: {t.description}" for t in TOOLS]),
433
- input="{current_task_input_placeholder}"
434
  )
435
 
436
  def agent_node(state: AgentState):
437
  current_task_actual_input = state.get('input', '')
438
- system_message_content = base_system_prompt_content.replace("{current_task_input_placeholder}", current_task_actual_input)
439
 
440
  messages_for_llm = [SystemMessage(content=system_message_content)]
 
 
 
441
  messages_for_llm.extend(state.get('messages', []))
442
 
443
  logger.debug(f"LangGraph agent_node - messages_for_llm: {messages_for_llm}")
444
- # Check the content of the first message (SystemMessage)
445
- if not messages_for_llm or not (isinstance(messages_for_llm, SystemMessage) and messages_for_llm.content and messages_for_llm.content.strip()):
446
- logger.error("LLM call would fail in agent_node: First message (SystemMessage) is empty or not valid.")
447
- return {"messages": [AIMessage(content="[ERROR] Agent node: Initial SystemMessage content is empty or invalid.")]}
448
 
449
- bound_llm = LLM_INSTANCE.bind_tools(TOOLS) # type: ignore
450
  response = bound_llm.invoke(messages_for_llm)
451
  return {"messages": [response]}
452
 
453
- if not LG_ToolExecutor_Class: raise ValueError("LG_ToolExecutor_Class is None for LangGraph.")
454
  tool_executor_instance_lg = LG_ToolExecutor_Class(tools=TOOLS)
455
 
456
- def tool_node(state: AgentState): # This custom function invokes tools from AIMessage
 
 
457
  last_msg = state['messages'][-1] if state.get('messages') and isinstance(state['messages'][-1], AIMessage) else None
458
  if not last_msg or not last_msg.tool_calls: return {"messages": []}
459
 
 
 
 
 
 
 
460
  tool_results = []
461
- for tc in last_msg.tool_calls:
462
  name, args, tc_id = tc.get('name'), tc.get('args'), tc.get('id')
463
  if not all([name, isinstance(args, dict), tc_id]):
464
  err_msg=f"Invalid tool_call: {tc}"; logger.error(err_msg)
465
  tool_results.append(ToolMessage(f"Error: {err_msg}", tool_call_id=tc_id or "error_id", name=name or "error_tool"))
466
  continue
467
  try:
468
- logger.info(f"LG Tool Invoking: '{name}' with {args} (ID: {tc_id})")
469
- # ToolNode expects a ToolInvocation if it's not being used as a direct runnable node for the whole tool step
470
- # However, if LG_ToolExecutor_Class is ToolNode, it's designed to take the list of tool_calls directly.
471
- # The issue was that LG_ToolInvocation itself might not be importable.
472
- # If LG_ToolExecutor_Class is ToolNode, it should handle the invocation from tool_calls.
473
- # Let's simplify and pass the tool_call directly if it's ToolNode
474
- if LG_ToolInvocation and type(LG_ToolExecutor_Class).__name__ != 'ToolNode':
475
- tool_invocation_obj_lg = LG_ToolInvocation(tool=name, tool_input=args)
476
- output_lg = tool_executor_instance_lg.invoke(tool_invocation_obj_lg) # type: ignore
477
- else: # Assume tool_executor_instance_lg (e.g. ToolNode) can handle the raw tool_call dict
478
- output_lg = tool_executor_instance_lg.invoke(tc) # type: ignore
479
 
480
  tool_results.append(ToolMessage(content=str(output_lg), tool_call_id=tc_id, name=name))
481
  except Exception as e_tool_node_lg:
@@ -483,12 +486,15 @@ def initialize_agent_and_tools(force_reinit=False):
483
  tool_results.append(ToolMessage(content=f"Error for tool {name}: {str(e_tool_node_lg)}", tool_call_id=tc_id, name=name))
484
  return {"messages": tool_results}
485
 
486
-
487
  workflow_lg = LG_StateGraph(AgentState) # type: ignore
488
  workflow_lg.add_node("agent", agent_node)
489
- # If LG_ToolExecutor_Class is ToolNode, tool_executor_instance_lg is a runnable node.
490
- # workflow_lg.add_node("tools", tool_executor_instance_lg) # This is the more standard LangGraph way for ToolNode
491
- workflow_lg.add_node("tools", tool_node) # Keeping custom for now due to ToolInvocation import issues
 
 
 
 
492
 
493
  workflow_lg.set_entry_point("agent")
494
  def should_continue_lg(state: AgentState): return "tools" if state['messages'][-1].tool_calls else LG_END
@@ -681,7 +687,7 @@ with gr.Blocks(css=".gradio-container {max-width:1280px !important;margin:auto !
681
  demo.load(update_ui_on_load_fn_within_context, [], [agent_status_display, missing_secrets_display])
682
 
683
  if __name__ == "__main__":
684
- logger.info(f"Application starting up (v7 - Final SafetySettings and Contents Fix)...")
685
  if not PYPDF2_AVAILABLE: logger.warning("PyPDF2 (PDF tool) NOT AVAILABLE.")
686
  if not PIL_TESSERACT_AVAILABLE: logger.warning("Pillow/Pytesseract (OCR tool) NOT AVAILABLE.")
687
  if not WHISPER_AVAILABLE: logger.warning("Whisper (Audio tool) NOT AVAILABLE.")
 
78
  try:
79
  from langgraph.graph import StateGraph, END
80
  try:
81
+ from langgraph.prebuilt import ToolNode
82
  LG_ToolExecutor_Class = ToolNode
83
  print("Using langgraph.prebuilt.ToolNode for LangGraph tool execution.")
84
  except ImportError:
85
  try:
86
+ from langgraph.prebuilt import ToolExecutor
87
  LG_ToolExecutor_Class = ToolExecutor
88
  print("Using langgraph.prebuilt.ToolExecutor (fallback) for LangGraph tool execution.")
89
  except ImportError as e_lg_exec_inner:
90
  print(f"Failed to import ToolNode and ToolExecutor from langgraph.prebuilt: {e_lg_exec_inner}")
91
  LG_ToolExecutor_Class = None
92
 
93
+ if LG_ToolExecutor_Class is not None:
94
  try:
95
  from langgraph.prebuilt import ToolInvocation as LGToolInvocationActual
96
  except ImportError:
97
  try:
98
+ from langgraph.tools import ToolInvocation as LGToolInvocationActual
99
+ print("Imported ToolInvocation from langgraph.tools")
100
  except ImportError as e_ti:
101
+ print(f"WARNING: Could not import ToolInvocation from langgraph.prebuilt or langgraph.tools: {e_ti}")
102
+ LGToolInvocationActual = None
103
 
104
+ if LGToolInvocationActual is not None or type(LG_ToolExecutor_Class).__name__ == 'ToolNode':
105
+ from langgraph.graph.message import add_messages as lg_add_messages
106
+ from langgraph.checkpoint.memory import MemorySaver as LGMemorySaver
 
 
 
 
107
  LANGGRAPH_FLAVOR_AVAILABLE = True
108
  LG_StateGraph, LG_END, LG_ToolInvocation, add_messages, MemorySaver_Class = \
109
  StateGraph, END, LGToolInvocationActual, lg_add_messages, LGMemorySaver # type: ignore
 
111
  else:
112
  LANGGRAPH_FLAVOR_AVAILABLE = False
113
  LG_StateGraph, LG_END, add_messages, MemorySaver_Class = (None,) * 4 # type: ignore
114
+ print(f"WARNING: LangGraph ToolInvocation not found and may be required by older ToolExecutor. LangGraph agent functionality might be limited or disabled.")
115
 
116
+ else:
117
  LANGGRAPH_FLAVOR_AVAILABLE = False
118
  LG_StateGraph, LG_END, LG_ToolInvocation, add_messages, MemorySaver_Class = (None,) * 5 # type: ignore
119
  print(f"WARNING: No suitable LangGraph tool executor (ToolNode/ToolExecutor) found. LangGraph agent will be disabled.")
120
 
121
+ except ImportError as e:
122
  LANGGRAPH_FLAVOR_AVAILABLE = False
123
  LG_StateGraph, LG_ToolExecutor_Class, LG_END, LG_ToolInvocation, add_messages, MemorySaver_Class = (None,) * 6
124
  print(f"WARNING: Core LangGraph components (like StateGraph, END) not found or import error: {e}. LangGraph agent will be disabled.")
 
255
  name_without_ext, current_ext = os.path.splitext(effective_save_path)
256
  if not current_ext:
257
  content_type_header = r.headers.get('content-type', '')
258
+ content_type_val = content_type_header.split(';')[0].strip() if content_type_header else ''
259
  if content_type_val:
260
  guessed_ext = mimetypes.guess_extension(content_type_val)
261
  if guessed_ext: effective_save_path += guessed_ext; logger.info(f"Added guessed ext: {guessed_ext}")
 
396
  model=GEMINI_MODEL_NAME,
397
  google_api_key=GOOGLE_API_KEY,
398
  temperature=0.0,
399
+ # safety_settings is removed to use model defaults to isolate "contents not specified" error
400
  timeout=120,
401
+ convert_system_message_to_human=False # Explicitly set to False
402
  )
403
  logger.info(f"LangChain LLM (Planner) initialized: {GEMINI_MODEL_NAME} (Using default safety settings, convert_system_message_to_human=False)")
404
  except Exception as e:
 
417
  except Exception as e: logger.warning(f"PythonREPLTool init failed: {e}")
418
  logger.info(f"Final tools list for agent: {[t.name for t in TOOLS]}")
419
 
420
+ if LANGGRAPH_FLAVOR_AVAILABLE and all([LG_StateGraph, LG_ToolExecutor_Class, LG_END, LLM_INSTANCE, add_messages]): # LG_ToolInvocation removed from check
421
  if not LANGGRAPH_MEMORY_SAVER and MemorySaver_Class: LANGGRAPH_MEMORY_SAVER = MemorySaver_Class(); logger.info("LangGraph MemorySaver initialized.")
422
  try:
423
  logger.info(f"Attempting LangGraph init (Tool Executor type: {LG_ToolExecutor_Class.__name__ if LG_ToolExecutor_Class else 'None'})")
424
  _TypedDict = getattr(__import__('typing_extensions'), 'TypedDict', dict)
425
  class AgentState(_TypedDict): input: str; messages: Annotated[List[Any], add_messages]
426
 
427
+ base_system_prompt_content_template_lg = LANGGRAPH_PROMPT_TEMPLATE_STR.format(
428
  tools="\n".join([f"- {t.name}: {t.description}" for t in TOOLS]),
429
+ input="{current_task_input_placeholder}"
430
  )
431
 
432
  def agent_node(state: AgentState):
433
  current_task_actual_input = state.get('input', '')
434
+ system_message_content = base_system_prompt_content_template_lg.replace("{current_task_input_placeholder}", current_task_actual_input)
435
 
436
  messages_for_llm = [SystemMessage(content=system_message_content)]
437
+ # If there's history, append it after the SystemMessage.
438
+ # The current_task_actual_input is now part of the SystemMessage.
439
+ # The 'messages' in state are previous AIMessages/ToolMessages.
440
  messages_for_llm.extend(state.get('messages', []))
441
 
442
  logger.debug(f"LangGraph agent_node - messages_for_llm: {messages_for_llm}")
443
+ # Check the first message (SystemMessage)
444
+ if not messages_for_llm or not (isinstance(messages_for_llm[0], SystemMessage) and messages_for_llm[0].content and str(messages_for_llm[0].content).strip()):
445
+ logger.error("LLM call would fail in agent_node: First message (SystemMessage) is missing, has no content, is not a string, or content is empty/whitespace.")
446
+ return {"messages": [AIMessage(content="[ERROR] Agent node: Initial SystemMessage content is invalid or empty.")]}
447
 
448
+ bound_llm = LLM_INSTANCE.bind_tools(TOOLS)
449
  response = bound_llm.invoke(messages_for_llm)
450
  return {"messages": [response]}
451
 
452
+ if not LG_ToolExecutor_Class: raise ValueError("LG_ToolExecutor_Class (ToolNode or ToolExecutor) is None for LangGraph.")
453
  tool_executor_instance_lg = LG_ToolExecutor_Class(tools=TOOLS)
454
 
455
+ # If LG_ToolExecutor_Class is ToolNode, it's often added directly to the graph.
456
+ # If we keep a custom tool_node, it needs to correctly use the tool_executor_instance_lg.
457
+ def tool_node(state: AgentState):
458
  last_msg = state['messages'][-1] if state.get('messages') and isinstance(state['messages'][-1], AIMessage) else None
459
  if not last_msg or not last_msg.tool_calls: return {"messages": []}
460
 
461
+ # ToolNode can often take the AIMessage directly, or a list of tool_calls
462
+ # The `invoke` method of ToolNode typically expects the full previous message or just tool_calls.
463
+ # Depending on the exact version and how ToolNode is implemented.
464
+ # The most straightforward is to let ToolNode handle the AIMessage's tool_calls.
465
+ # This implies tool_executor_instance_lg should be the node itself.
466
+ # However, if we must use a custom function:
467
  tool_results = []
468
+ for tc in last_msg.tool_calls: # tc is a dict from AIMessage.tool_calls
469
  name, args, tc_id = tc.get('name'), tc.get('args'), tc.get('id')
470
  if not all([name, isinstance(args, dict), tc_id]):
471
  err_msg=f"Invalid tool_call: {tc}"; logger.error(err_msg)
472
  tool_results.append(ToolMessage(f"Error: {err_msg}", tool_call_id=tc_id or "error_id", name=name or "error_tool"))
473
  continue
474
  try:
475
+ logger.info(f"LG Tool Invoking via custom tool_node: '{name}' with {args} (ID: {tc_id})")
476
+ # Construct ToolInvocation if LG_ToolInvocation is available and needed by the executor_instance
477
+ if LG_ToolInvocation and not isinstance(tool_executor_instance_lg, ToolNode): # ToolNode might not need this
478
+ invocation = LG_ToolInvocation(tool=name, tool_input=args)
479
+ output_lg = tool_executor_instance_lg.invoke(invocation) # type: ignore
480
+ else: # Assume ToolNode or compatible executor can take the dict directly
481
+ output_lg = tool_executor_instance_lg.invoke(tc) # Pass the tool_call dict
 
 
 
 
482
 
483
  tool_results.append(ToolMessage(content=str(output_lg), tool_call_id=tc_id, name=name))
484
  except Exception as e_tool_node_lg:
 
486
  tool_results.append(ToolMessage(content=f"Error for tool {name}: {str(e_tool_node_lg)}", tool_call_id=tc_id, name=name))
487
  return {"messages": tool_results}
488
 
 
489
  workflow_lg = LG_StateGraph(AgentState) # type: ignore
490
  workflow_lg.add_node("agent", agent_node)
491
+ # If LG_ToolExecutor_Class is ToolNode, use the instance directly
492
+ if type(LG_ToolExecutor_Class).__name__ == 'ToolNode':
493
+ workflow_lg.add_node("tools", tool_executor_instance_lg)
494
+ logger.info("Added ToolNode instance directly to LangGraph.")
495
+ else: # Fallback to custom tool_node (might be needed for older ToolExecutor)
496
+ workflow_lg.add_node("tools", tool_node)
497
+ logger.info("Added custom tool_node function to LangGraph.")
498
 
499
  workflow_lg.set_entry_point("agent")
500
  def should_continue_lg(state: AgentState): return "tools" if state['messages'][-1].tool_calls else LG_END
 
687
  demo.load(update_ui_on_load_fn_within_context, [], [agent_status_display, missing_secrets_display])
688
 
689
  if __name__ == "__main__":
690
+ logger.info(f"Application starting up (v7.1 - Agent Node Message Structure Change)...")
691
  if not PYPDF2_AVAILABLE: logger.warning("PyPDF2 (PDF tool) NOT AVAILABLE.")
692
  if not PIL_TESSERACT_AVAILABLE: logger.warning("Pillow/Pytesseract (OCR tool) NOT AVAILABLE.")
693
  if not WHISPER_AVAILABLE: logger.warning("Whisper (Audio tool) NOT AVAILABLE.")