1919 openai_chat_stream ,
2020 llm_chat ,
2121 llm_chat_stream ,
22- is_claude_model ,
22+ is_gemini_model ,
2323 openai_upload_file ,
2424 openai_create_vector_store ,
2525 openai_wait_for_vector_store_files ,
2626 openai_add_files_to_vector_store ,
27+ openai_search_vector_store ,
2728)
2829from tutor_prompts import DEFAULT_USER_PROFILE
2930from brave_images import BraveImageError , search_brave_images , verify_image_url
251252 },
252253 },
253254 },
255+ {
256+ "type" : "function" ,
257+ "function" : {
258+ "name" : "search_uploaded_documents" ,
259+ "description" : (
260+ "Search chunks from user-uploaded documents already indexed for this session. "
261+ "Use this before asking the user to paste document content. "
262+ "Best for questions about attached PDFs/docs."
263+ ),
264+ "parameters" : {
265+ "type" : "object" ,
266+ "properties" : {
267+ "query" : {
268+ "type" : "string" ,
269+ "description" : "The question or search query to run against uploaded documents." ,
270+ },
271+ "max_results" : {
272+ "type" : "integer" ,
273+ "description" : "Maximum number of chunks to return (1-10)." ,
274+ "default" : 6 ,
275+ },
276+ },
277+ "required" : ["query" ],
278+ },
279+ },
280+ },
254281 {
255282 "type" : "function" ,
256283 "function" : {
@@ -653,16 +680,22 @@ def _looks_like_file_access_failure(text):
653680 return any (token in cleaned for token in indicators )
654681
655682
656- def _tools_for_request (web_search = False ):
683+ def _tools_for_request (web_search = False , include_uploaded_documents_search = False ):
657684 """
658685 Build tool list for this run.
659686
660- Always returns the full tool set. Image-search function tools
687+ Returns the tool set for this run . Image-search function tools
661688 (search_brave_images, search_wiki_images) must remain available even
662689 when web_search is enabled because OpenAI's built-in web_search does
663690 not return images.
664691 """
665- return TOOLS
692+ if include_uploaded_documents_search :
693+ return TOOLS
694+ return [
695+ tool
696+ for tool in TOOLS
697+ if str ((tool .get ("function" ) or {}).get ("name" ) or "" ).strip () != "search_uploaded_documents"
698+ ]
666699
667700
668701def _append_reasoning_summary (block , reasoning ):
@@ -804,6 +837,8 @@ def assess_context_or_answer_simple(
804837 system_prompt = get_pre_flight_prompt ()
805838
806839 _PRE_FLIGHT_TOOL_NAMES = {"update_learner_profile" , "lookup_platform_help" }
840+ if is_gemini_model (model ) and enable_file_search and vector_store_id :
841+ _PRE_FLIGHT_TOOL_NAMES .add ("search_uploaded_documents" )
807842 tools = [t for t in TOOLS if t ["function" ]["name" ] in _PRE_FLIGHT_TOOL_NAMES ]
808843
809844 # Append current profile context
@@ -822,6 +857,16 @@ def assess_context_or_answer_simple(
822857 ),
823858 }
824859 )
860+ if is_gemini_model (model ):
861+ messages .append (
862+ {
863+ "role" : "system" ,
864+ "content" : (
865+ "When document-specific evidence is needed, call search_uploaded_documents "
866+ "before asking the user for manual excerpts."
867+ ),
868+ }
869+ )
825870
826871 if session_messages :
827872 # We append the conversation history, which already contains the user's latest message
@@ -1049,6 +1094,40 @@ def execute_tool(name, arguments, session=None):
10491094 "current_profile" : current_profile ,
10501095 }
10511096
1097+ elif name == "search_uploaded_documents" :
1098+ query = str (arguments .get ("query" ) or "" ).strip ()
1099+ if not query :
1100+ return {"success" : False , "error" : "Missing required field: query" }
1101+
1102+ vector_store_id = str (arguments .get ("vector_store_id" ) or "" ).strip ()
1103+ if not vector_store_id and session is not None :
1104+ vector_store_id = str (getattr (session , "openai_vector_store_id" , "" ) or "" ).strip ()
1105+ if not vector_store_id :
1106+ return {"success" : False , "error" : "No uploaded-document index is available for this session." }
1107+
1108+ max_results = arguments .get ("max_results" , 6 )
1109+ try :
1110+ max_results = int (max_results )
1111+ except (TypeError , ValueError ):
1112+ max_results = 6
1113+ max_results = max (1 , min (max_results , 10 ))
1114+
1115+ try :
1116+ payload = openai_search_vector_store (
1117+ vector_store_id = vector_store_id ,
1118+ query = query ,
1119+ max_results = max_results ,
1120+ )
1121+ chunks = payload .get ("results" ) if isinstance (payload , dict ) else []
1122+ return {
1123+ "success" : True ,
1124+ "vector_store_id" : vector_store_id ,
1125+ "query" : query ,
1126+ "results" : chunks if isinstance (chunks , list ) else [],
1127+ }
1128+ except Exception as e :
1129+ return {"success" : False , "error" : f"Document search failed: { e } " }
1130+
10521131 elif name == "create_main_block" :
10531132 title = str (arguments .get ("title" ) or "" ).strip ()
10541133 summary = str (arguments .get ("summary" ) or "" ).strip ()
@@ -1589,6 +1668,7 @@ def run_agent_with_session(
15891668 from models import Block
15901669
15911670 model = model or os .getenv ("OPENAI_MODEL" , "gpt-5.2" )
1671+ gemini_request = is_gemini_model (model )
15921672
15931673 # Add user message to session, including attachment metadata for persistence.
15941674 session .add_user_message (user_message , attachments = attachment_metadata )
@@ -1674,7 +1754,7 @@ def _mark_attachment(
16741754 vector_store_id = getattr (session , "openai_vector_store_id" , None )
16751755 enable_file_search = False
16761756
1677- if searchable_files and model .startswith ("gpt" ):
1757+ if searchable_files and ( model .startswith ("gpt" ) or gemini_request ):
16781758 upload_records = []
16791759 for sf in searchable_files :
16801760 filename = str (sf .get ("filename" ) or "file" )
@@ -1822,14 +1902,24 @@ def _mark_attachment(
18221902
18231903 if vector_store_id :
18241904 enable_file_search = True
1825- _insert_system_instruction (
1826- messages ,
1827- (
1828- "File search context is available for this session. "
1829- "When the user asks about uploaded documents, use file_search results first. "
1830- "Do not ask the user to paste the document text unless retrieval fails."
1831- ),
1832- )
1905+ if gemini_request :
1906+ _insert_system_instruction (
1907+ messages ,
1908+ (
1909+ "Uploaded document retrieval is available via search_uploaded_documents. "
1910+ "Use it before asking the user to paste document text. "
1911+ "When citing results, include relevant excerpt details."
1912+ ),
1913+ )
1914+ else :
1915+ _insert_system_instruction (
1916+ messages ,
1917+ (
1918+ "File search context is available for this session. "
1919+ "When the user asks about uploaded documents, use file_search results first. "
1920+ "Do not ask the user to paste the document text unless retrieval fails."
1921+ ),
1922+ )
18331923
18341924 # --- Pre-Flight Check ---
18351925 # Only if this is a main block generation (depth 0)
@@ -1878,7 +1968,10 @@ def _mark_attachment(
18781968 return response_block
18791969
18801970 forced_tool_choice = None
1881- active_tools = _tools_for_request (web_search )
1971+ active_tools = _tools_for_request (
1972+ web_search ,
1973+ include_uploaded_documents_search = bool (gemini_request and vector_store_id ),
1974+ )
18821975 created_main_block = None
18831976 should_stream = callable (stream_text_callback ) or callable (stream_reasoning_callback )
18841977
0 commit comments