diff --git a/electron/servers/fastapi/constants/llm.py b/electron/servers/fastapi/constants/llm.py index 21eacb73..3f663f6c 100644 --- a/electron/servers/fastapi/constants/llm.py +++ b/electron/servers/fastapi/constants/llm.py @@ -4,4 +4,4 @@ OPENAI_URL = "https://api.openai.com/v1" DEFAULT_OPENAI_MODEL = "gpt-4.1" DEFAULT_GOOGLE_MODEL = "models/gemini-2.5-flash" DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-20250514" -DEFAULT_CODEX_MODEL = "gpt-5.2-codex" +DEFAULT_CODEX_MODEL = "gpt-5.1-codex-mini" diff --git a/electron/servers/fastapi/services/llm_client.py b/electron/servers/fastapi/services/llm_client.py index 1e28f521..6f5c7bac 100644 --- a/electron/servers/fastapi/services/llm_client.py +++ b/electron/servers/fastapi/services/llm_client.py @@ -100,6 +100,55 @@ class LLMClient: return False return parse_bool_or_none(get_web_grounding_env()) or False + def web_search_enabled_for_request(self, web_search: bool) -> bool: + """Attach SearchWebTool only when the user enabled web search for this request. + + Controlled solely by the presentation ``web_search`` flag (Advanced settings). + Legacy ``WEB_GROUNDING`` / settings toggles are not consulted here so a saved + false there cannot disable per-deck web search. + """ + if not web_search: + return False + if self.llm_provider in (LLMProvider.OLLAMA, LLMProvider.CUSTOM): + return False + return True + + def outline_uses_prefetched_web_facts(self, web_search: bool) -> bool: + """Chat Completions + json_schema rarely invoke custom function tools. + + For OpenAI and Codex we prefetch via the Responses API (``web_search_preview``) + and attach the result as context so Advanced settings **Web search** still + grounds outlines without relying on ``SearchWebTool`` in the same call. + """ + if not self.web_search_enabled_for_request(web_search): + return False + return self.llm_provider in (LLMProvider.OPENAI, LLMProvider.CODEX) + + async def prefetch_outline_web_facts( + self, + content: str, + additional_context: Optional[str] = None, + ) -> Optional[str]: + if self.llm_provider not in (LLMProvider.OPENAI, LLMProvider.CODEX): + return None + parts = [(content or "").strip(), (additional_context or "").strip()] + topic = "\n\n".join(p for p in parts if p) + if not topic: + topic = "general presentation topic" + topic = topic[:12000] + query = ( + "Search the web and summarize the most relevant current facts, statistics, " + "and notable recent developments for this presentation topic. Use concise " + "bullet points; include approximate dates or time ranges when known.\n\n" + f"Topic:\n{topic}" + ) + try: + text = await self._search_openai(query) + out = (text or "").strip() + return out or None + except Exception: + return None + # ? Disable thinking def disable_thinking(self) -> bool: return parse_bool_or_none(get_disable_thinking_env()) or False @@ -1753,7 +1802,7 @@ class LLMClient: current_arguments = None has_response_schema_tool_call = False - async for event in await client.chat.completions.create( + completion_kwargs: Dict[str, Any] = dict( model=model, messages=[message.model_dump() for message in messages], max_completion_tokens=max_tokens, @@ -1774,7 +1823,11 @@ class LLMClient: ), extra_body=extra_body, stream=True, - ): + ) + if all_tools: + completion_kwargs["tool_choice"] = "auto" + completion_kwargs["parallel_tool_calls"] = True + async for event in await client.chat.completions.create(**completion_kwargs): event: OpenAIChatCompletionChunk = event if not event.choices: continue diff --git a/electron/servers/fastapi/services/llm_tool_calls_handler.py b/electron/servers/fastapi/services/llm_tool_calls_handler.py index 63476028..f396112d 100644 --- a/electron/servers/fastapi/services/llm_tool_calls_handler.py +++ b/electron/servers/fastapi/services/llm_tool_calls_handler.py @@ -55,7 +55,7 @@ class LLMToolCallsHandler: self.dynamic_tools.append(tool) match self.client.llm_provider: - case LLMProvider.OPENAI | LLMProvider.OLLAMA | LLMProvider.CUSTOM: + case LLMProvider.OPENAI | LLMProvider.OLLAMA | LLMProvider.CUSTOM | LLMProvider.CODEX: return self.parse_tool_openai(tool, strict) case LLMProvider.ANTHROPIC: return self.parse_tool_anthropic(tool) @@ -63,7 +63,7 @@ class LLMToolCallsHandler: return self.parse_tool_google(tool) case _: raise ValueError( - f"LLM provider must be either openai, anthropic, or google" + "LLM provider must be one of: openai, anthropic, google, codex, ollama, custom" ) def parse_tool_openai( @@ -181,7 +181,7 @@ class LLMToolCallsHandler: # Search web tool call handler async def search_web_tool_call_handler(self, arguments: str) -> str: match self.client.llm_provider: - case LLMProvider.OPENAI: + case LLMProvider.OPENAI | LLMProvider.CODEX: return await self.search_web_tool_call_handler_openai(arguments) case LLMProvider.ANTHROPIC: return await self.search_web_tool_call_handler_anthropic(arguments) diff --git a/electron/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py b/electron/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py index 13bf3a14..3d0d1d49 100644 --- a/electron/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py +++ b/electron/servers/fastapi/utils/llm_calls/generate_presentation_outlines.py @@ -16,6 +16,7 @@ def get_system_prompt( instructions: Optional[str] = None, include_title_slide: bool = True, include_table_of_contents: bool = False, + web_search: bool = False, ): verbosity_instruction = ( "Slide content should be abound 20 words but detailed enough to generate a good slide." @@ -40,6 +41,27 @@ def get_system_prompt( ) toc_block = f"{toc_instruction}\n" if toc_instruction else "" + if web_search: + tools_hint = "Try to use available tools when they improve accuracy.\n" + web_block = ( + "Web search is enabled: use any \"## Web research (current sources)\" section in Context when present, " + "and call SearchWebTool when it is available for fresh facts.\n" + ) + else: + tools_hint = "" + web_block = "Do not use web search for this outline; rely on Content and Context only.\n" + + url_line = ( + "Only include URLs if they appear in Content, Context, or a \"## Web research (current sources)\" block.\n" + if web_search + else "Only include URLs if they appear in the provided content/context.\n" + ) + data_line = ( + "Ground slide data in Content and Context, and in \"## Web research (current sources)\" when that block is present.\n" + if web_search + else "Make sure data used is strictly from the provided content/context.\n" + ) + slide_outline_structure = ( "Each slide content:\n" " - Must have a ## title.\n" @@ -60,11 +82,13 @@ def get_system_prompt( "If 'auto-detect' is used, figure it out from the content/context.\n" f"{title_slide_instruction}\n" f"{toc_block}" + f"{tools_hint}" + f"{web_block}" f"{slide_outline_structure}\n" "Slide content must not contain any presentation branding/styling information.\n" "Title slide must only contain title, presenter name, date and overview.\n" - "Only include URLs if they appear in the provided content/context.\n" - "Make sure data used is strictly from the provided content/context.\n" + f"{url_line}" + f"{data_line}" "Make sure data is consistent across all slides." ) @@ -124,6 +148,7 @@ def get_messages( instructions: Optional[str] = None, include_title_slide: bool = True, include_table_of_contents: bool = False, + web_search: bool = False, ): return [ LLMSystemMessage( @@ -133,6 +158,7 @@ def get_messages( instructions, include_title_slide, include_table_of_contents, + web_search=web_search, ), ), LLMUserMessage( @@ -171,6 +197,21 @@ async def generate_ppt_outline( client = LLMClient() + merged_context = additional_context + if client.outline_uses_prefetched_web_facts(web_search): + facts = await client.prefetch_outline_web_facts(content, additional_context) + if facts: + merged_context = ( + f"{(additional_context or '').strip()}\n\n## Web research (current sources)\n{facts}" + if (additional_context or "").strip() + else f"## Web research (current sources)\n{facts}" + ) + + use_search_tool = ( + client.web_search_enabled_for_request(web_search) + and not client.outline_uses_prefetched_web_facts(web_search) + ) + try: async for chunk in client.stream_structured( model, @@ -178,20 +219,17 @@ async def generate_ppt_outline( content, n_slides, language, - additional_context, + merged_context, tone, verbosity, instructions, include_title_slide, include_table_of_contents, + web_search=bool(web_search), ), response_model.model_json_schema(), strict=True, - tools=( - [SearchWebTool] - if (client.enable_web_grounding() and web_search) - else None - ), + tools=([SearchWebTool] if use_search_tool else None), ): yield chunk except Exception as e: diff --git a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingCodex.tsx b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingCodex.tsx index 8b4e8b9a..756e4317 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingCodex.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingCodex.tsx @@ -46,16 +46,16 @@ interface CodexModel { } const CHATGPT_MODELS: CodexModel[] = [ - { id: "gpt-5.1", name: "GPT-5.1" }, - { id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max" }, - { id: "gpt-5.2", name: "GPT-5.2" }, - { id: "gpt-5.2-codex", name: "GPT-5.2 Codex" }, - { id: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, - { id: "gpt-5.4-mini", name: "GPT-5.4 Mini" }, { id: "gpt-5.4", name: "GPT-5.4" }, + { id: "gpt-5.2-codex", name: "GPT-5.2-Codex" }, + { id: "gpt-5.1-codex-max", name: "GPT-5.1-Codex-Max" }, + { id: "gpt-5.4-mini", name: "GPT-5.4-Mini" }, + { id: "gpt-5.3-codex", name: "GPT-5.3-Codex" }, + { id: "gpt-5.2", name: "GPT-5.2" }, + { id: "gpt-5.1-codex-mini", name: "GPT-5.1-Codex-Mini" }, ]; -const DEFAULT_CODEX_MODEL = "gpt-5.4-mini"; +const DEFAULT_CODEX_MODEL = "gpt-5.1-codex-mini"; export default function CodexConfig({ codexModel, diff --git a/electron/servers/nextjs/app/(presentation-generator)/upload/components/AdvanceSettings.tsx b/electron/servers/nextjs/app/(presentation-generator)/upload/components/AdvanceSettings.tsx index bd709a5c..59b226c6 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/upload/components/AdvanceSettings.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/upload/components/AdvanceSettings.tsx @@ -142,7 +142,7 @@ const AdvanceSettings = ({ config, onConfigChange }: ConfigurationSelectsProps) onCheckedChange={(checked) => setAdvancedDraft((prev) => ({ ...prev, webSearch: checked }))} /> -

Allow the model to consult the web for fresher facts.

+

Allow the model to consult the web for fresher facts. Turn on and click Save—this toggle alone controls web search for this deck.

{/* Instructions */} diff --git a/electron/servers/nextjs/app/(presentation-generator)/upload/components/ConfigurationSelects.tsx b/electron/servers/nextjs/app/(presentation-generator)/upload/components/ConfigurationSelects.tsx index e2040136..b56eb286 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/upload/components/ConfigurationSelects.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/upload/components/ConfigurationSelects.tsx @@ -375,7 +375,7 @@ export function ConfigurationSelects({ onCheckedChange={(checked) => setAdvancedDraft((prev) => ({ ...prev, webSearch: checked }))} /> -

Allow the model to consult the web for fresher facts.

+

Allow the model to consult the web for fresher facts. Turn on and click Save—this toggle alone controls web search for this deck.

{/* Instructions */} diff --git a/electron/servers/nextjs/components/CodexConfig.tsx b/electron/servers/nextjs/components/CodexConfig.tsx index 3f6453d2..da56a161 100644 --- a/electron/servers/nextjs/components/CodexConfig.tsx +++ b/electron/servers/nextjs/components/CodexConfig.tsx @@ -33,16 +33,16 @@ interface CodexModel { } export const CHATGPT_MODELS: CodexModel[] = [ - { id: "gpt-5.1", name: "GPT-5.1" }, - { id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max" }, - { id: "gpt-5.2", name: "GPT-5.2" }, - { id: "gpt-5.2-codex", name: "GPT-5.2 Codex" }, - { id: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, - { id: "gpt-5.4 mini", name: "GPT-5.4 Mini" }, { id: "gpt-5.4", name: "GPT-5.4" }, + { id: "gpt-5.2-codex", name: "GPT-5.2-Codex" }, + { id: "gpt-5.1-codex-max", name: "GPT-5.1-Codex-Max" }, + { id: "gpt-5.4-mini", name: "GPT-5.4-Mini" }, + { id: "gpt-5.3-codex", name: "GPT-5.3-Codex" }, + { id: "gpt-5.2", name: "GPT-5.2" }, + { id: "gpt-5.1-codex-mini", name: "GPT-5.1-Codex-Mini" }, ]; -export const DEFAULT_CODEX_MODEL = "gpt-5.4-mini"; +export const DEFAULT_CODEX_MODEL = "gpt-5.1-codex-mini"; export default function CodexConfig({ codexModel,