Skip to content

Commit 27b6644

Browse files
committed
Add Gemini model routing, tools, streaming, and doc-retrieval bridge
1 parent c15ec06 commit 27b6644

8 files changed

Lines changed: 878 additions & 35 deletions

File tree

backend/agent.py

Lines changed: 107 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@
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
)
2829
from tutor_prompts import DEFAULT_USER_PROFILE
2930
from brave_images import BraveImageError, search_brave_images, verify_image_url
@@ -251,6 +252,32 @@
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

668701
def _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

backend/lambda_handler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,8 @@ def _emit_trace_event(trace_event: str, **fields) -> None:
143143
)
144144

145145
# Allowed LLM models that users may select
146-
ALLOWED_MODELS = {"gpt-5.2", "claude-sonnet-4-6", "gemini-2.5-pro"}
147-
_ALLOWED_MODEL_PREFIXES = ()
146+
ALLOWED_MODELS = {"gpt-5.2", "claude-sonnet-4-6", "gemini-3.1-pro-preview"}
147+
_ALLOWED_MODEL_PREFIXES = ("gemini-",)
148148

149149

150150
def _is_allowed_model(model_name: str) -> bool:

0 commit comments

Comments
 (0)